Intro To Manim - ManimCE Professional Course
Course Introduction video [https://www.youtube.com/watch?v=RN8el9uNioc&t=13s]
Course Introduction video [https://www.youtube.com/watch?v=RN8el9uNioc&t=13s]
Watch installation videos [https://www.youtube.com/watch?v=CYOLQk8GpME&t=2s]
ManimCE has three versions, each version uses a different graphics engine: PyCairo, OpenGL and WebGL. The OpenGL and WebGL versions do not yet have stable versions, so in this course the version used by PyCairo will be studied.
ManimCE + PyCairo works as follows:
Python reads your script.
Manim’s library converts its language to PyCairo language, and renders every frame.
After all the images are generated, Manim calls FFmpeg to concatenate them into a video.
You can see the complete process in this image.
The basic structure of any Manim script is very simple:
# Import manim library
from manim import *
class SceneName(Scene):
"""
By convention, the structure of your animation
is defined in the construct method, but later
we will learn how to do it in different ways.
"""
def construct(self):
# Create a text using Text class
text = Text("Hello world")
# Text animation
self.play(Write(text))
# Pause
self.wait()
First you import the library: from manim import *
Create a class with the name you want to give to your .mp4 file, this class must inherit from the Scene class.
Create a construct method.
Define your animation structure within the construct method.
Save your script with some name, with the extension .py, let’s say, my_script.py
To render your code you must activate your virtual environment and use this command (or some variation):
manim my_script.py SceneName -pql
manim: It is the command that calls the ManimCE library.
my_script.py: It is the name of your script.
SceneName: It is the name of your class that inherits from the Scene class.
-pql: They are the flags that indicate the resolution of the video.
p: Preview - Indicates that when the video is finished rendering, it will automatically open with your default video software. I recommend using mpv [https://mpv.io/].
q: Quality - Indicates that the next command will define one of the resolutions that come by default in Manim.
l: Low resolution - It indicates that the video will be rendered at 854x480 at 15 fps (REMARK this command must go after theqflag, otherwise it won’t work). You can replace this flag with these other three:
m: Medium resolution: 1280x720 at 30FPS.
h: High resolution: 1920x1080 at 60FPS.
k: 4K resolution: 3840x2160 at 60FPS.
As you can guess, you can define several classes that inherit from the Scene class in your .py file, and when rendering it you simply have to indicate their name on the command line after typing the name of the .py file.
Result:
Warning
Keep in mind that if you render a video that you have already rendered before, this new one will overwrite the previous one, one way to avoid this is by using as an additional flag:
-o FILE_NAME
Where FILE_NAME is the name you want your .mp4 file to have.
The objects that can be displayed on the screen are called Mobjects, and there are several types, but the most important are the following three:
Mobject: This is the abstract class that generates all the following Mobjects.
ImageMobject: They are raster images, that is, bitmaps, such as PNGs, JPGs, etc. At the moment Manim does not have support for GIFs.
VMobject: They are vector figures, in general, Bézier curves, these are the most flexible Mobjects, since you can define your own VMobjects yourself or use the ones that come by default.
Group and VGroup: They are groups of Mobjects and VMobjects respectively, by grouping several Mobjects/VMobjects it allows us to modify these groups in a simpler and faster way.
The Mobjects that we are going to use the most in the basic sections will be subclasses of VMobjects, these are: lines Line, circles Circle, squares Square, texts Text, etc.
It is important to note that the texts are not “pure” VMobjects, the texts are imported SVG that Manim generates using the fonts of your system, the same happens with the texts generated in LaTeX.
We will study the texts in detail in Section 7, the important thing for now is to understand that the texts are SVG that Manim reads and converts them into VMobjects.
In order to use Manim with Jupyter Notebook you must install jupyter in your virtual environment, using:
pip3 install jupyter
To use it simply run in your terminal:
jupyter-notebook
Create a new file, select Python 3 (ipykernel):
And use it in the following way:
If you use VSCode you can also use it together with Jupyter-Notebook, you just have to indicate your virtual environment to VSCode.
If you are a beginner it is a good idea to use this tool so that your learning is more comfortable.
There are two ways to add an object to the screen, either with an animation (as we will see below), or by using the Scene.add() method.
This method adds a Mobject instantly, without animation or duration.
If a scene has no duration (that is, it has no animations or pauses) then Manim will render this scene as an image.
Try rendering the following scene with the -pql flags.
class SceneWithoutDuration(Scene):
def construct(self):
sq = Square()
self.add(sq)
# self.wait()
If you try, you won’t get an .mp4 file, as this scene has no duration. If you uncomment the last line then you will get an .mp4 as a result, since the animation will last 1 second.
To get the last frame of a scene (that is, not render it as .mp4, but as PNG image) then use the -ps flags instad -pql.
There are two ways to make animations, one is with updaters and the other is using the Scene.play method. The updaters are more advanced and we will see them later.
Scene.playThe Scene.play method accepts two types of arguments, either methods of Mobjects as animations and class animations, we will see the methods of Mobjects later after seeing Mobjects properties, the most basic animations are the class animations.
In summary, the ways to make animations are:
Updaters:
Simple updaters
\(\alpha\) updaters
\(\tt dt\) updaters
Scene.play:
Methods of Mobjects
Class animations
In this section we will analyze the last one, which is the simplest.
There are already several predefined class animations, and they are divided into 4 groups.
Creation animations: They add an object to the screen.
Indication animations: They help to identify an object on the screen, they are actually a fusion of the other 3 animations.
Transformation animations: They modify the shape or position of an object.
Animations to remove: They remove an object from the screen.
Every class animation has the following main properties:
run_time (float): It is the duration of the animation.
rate_func (function): It is the behavior of the animation, most of them are smooth, but they can also be:
def linear(t: typing.Union[np.ndarray, float]) -> typing.Union[np.ndarray, float]:
return t
def smooth(t: float, inflection: float = 10.0) -> np.ndarray:
error = sigmoid(-inflection / 2)
return np.clip(
(sigmoid(inflection * (t - 0.5)) - error) / (1 - 2 * error),
0,
1,
)
def rush_into(t: float, inflection: float = 10.0) -> np.ndarray:
return 2 * smooth(t / 2.0, inflection)
def rush_from(t: float, inflection: float = 10.0) -> np.ndarray:
return 2 * smooth(t / 2.0 + 0.5, inflection) - 1
def slow_into(t: np.ndarray) -> np.ndarray:
return np.sqrt(1 - (1 - t) * (1 - t))
def double_smooth(t: float) -> np.ndarray:
if t < 0.5:
return 0.5 * smooth(2 * t)
else:
return 0.5 * (1 + smooth(2 * t - 1))
def there_and_back(t: float, inflection: float = 10.0) -> np.ndarray:
new_t = 2 * t if t < 0.5 else 2 * (1 - t)
return smooth(new_t, inflection)
def there_and_back_with_pause(t: float, pause_ratio: float = 1.0 / 3) -> np.ndarray:
a = 1.0 / pause_ratio
if t < 0.5 - pause_ratio / 2:
return smooth(a * t)
elif t < 0.5 + pause_ratio / 2:
return 1
else:
return smooth(a - a * t)
def running_start(t: float, pull_factor: float = -0.5) -> typing.Iterable:
return bezier([0, 0, pull_factor, pull_factor, 1, 1, 1])(t)
def not_quite_there(
func: typing.Callable[[float, typing.Optional[float]], np.ndarray] = smooth,
proportion: float = 0.7,
) -> typing.Callable[[float], np.ndarray]:
def result(t):
return proportion * func(t)
return result
def wiggle(t: float, wiggles: float = 2) -> np.ndarray:
return there_and_back(t) * np.sin(wiggles * np.pi * t)
def squish_rate_func(
func: typing.Callable[[float], typing.Any],
a: float = 0.4,
b: float = 0.6,
) -> typing.Callable[[float], typing.Any]: # what is func return type?
def result(t):
if a == b:
return a
if t < a:
return func(0)
elif t > b:
return func(1)
else:
return func((t - a) / (b - a))
return result
# See all in manim/utils/rate_functions.py
remover (bool): It is True when the intention of the animation is to remove the Mobject from the screen, some class animations that have this attr as True are FadeOut and Uncreate.
Mobject (Mobject): Some animations only accept VMobjects (like Write) or some specific Mobject for that animation, while others accept any Mobject (like FadeIn).
There are some rules for using Scene.play, one of the most important is that you cannot animate a Mobject twice in the same Scene.play method.
In the example below, you couldn’t have more than one uncommented animation.
class BasicAnimations(Scene):
def construct(self):
text = Text("Hello word")
self.play(
Write(text),
# FadeIn(text),
# GrowFromCenter(text),
# FadeInFromLarge(text, scale_factor=2), # <- Some animations require more arguments to work.
)
self.wait() # <- It is advisable to always have a wait at the end
And in general, it is always advisable to have a Scene.wait at the end so that the above animation does not have strange behaviors.
class MultipleAnimationSameTime(Scene):
def construct(self):
square = Square()
circle = Circle()
self.play(
Create(square),
FadeIn(circle)
)
self.wait()
Of all animations:
class ChangeDuration(Scene):
def construct(self):
self.play(
Create(Circle()),
FadeIn(Square()),
run_time=3,
rate_func=linear
)
self.wait()
Of each animation:
class ChangeDurationMultipleAnimations(Scene):
def construct(self):
self.play(
Create(
Circle(),
run_time=3,
rate_func=smooth
),
FadeIn(
Square(),
run_time=2,
rate_func=there_and_back
),
GrowFromCenter(
Triangle()
)
)
self.wait()
class MoreAnimations(Scene):
def construct(self):
text = Text("Hello world")
self.play(Write(text))
self.wait()
self.play(Rotate(text,PI/2))
self.wait()
self.play(Indicate(text))
self.wait()
self.play(FocusOn(text))
self.wait()
self.play(ShowCreationThenDestructionAround(text))
self.wait()
self.play(FadeOut(text))
self.wait()
Some time ago I made this small documentation [https://elteoremadebeethoven.github.io/manim_3feb_docs.github.io/html/tree/animation.html#animations] of all ManimCairo animations, the use of animations is not the same in ManimCE but you can get an idea of how they work.
The source code of all class animations are in manim/animations.
If you want to see a tree of what inheritance is like between Mobjects you can see the following diagram (created by the ManimCE community).
Before studying the attributes of Mobjects it is necessary to know the basic characteristics of the camera.
By default, the Camera is 8 units high, and since the aspect ratio is 16/9, the width can be easily calculated. These 8 units are independent of the rendering resolution (480p, 720p, etc), so there is no need to worry about that.
The coordinates of each Mobject are a three-dimensional array, and the coordinate \([0,0,0]\) is located in the center of the camera.
For now, we will not worry about the 3rd dimension, we will focus only on the x, y coordinates.
Note
These values can be changed but it is not recommended.
All Mobjects have four main attributes:
Position
Width
Height
Z index
We won’t study Z index in this section until Section 4.
To start studying the properties we will create a rectangle and add it on the screen, I recommend the student to use Jupyter-Notebook to study these first sections. We will omit the scene name to save space.
def construct(self):
req = Rectangle()
self.add(req)
Manim already has some constants that will help us locate our Mobjects, which are:
Origin:
ORIGIN = np.array([ 0, 0, 0])
One-dimensional vectors:
UP = np.array([ 1, 0, 0])
DOWN = np.array([-1, 0, 0])
RIGHT = np.array([ 0, 1, 0])
LEFT = np.array([ 0,-1, 0])
Two-dimensional vectors:
UR = np.array([ 1, 1, 0])
UL = np.array([-1, 1, 0])
DR = np.array([ 1,-1, 0])
DL = np.array([-1,-1, 0])
When we instantiate a Mobject, it is always located by default in the center of the screen, that is, in the location \([0,0,0]\).
If we want to place this object in another position, there are two positioning systems:
Absolute Position: Use as reference the coordinates of the camera or the current location of our Mobject.
Mobject.move_to()
Mobject.shift()
Relative Position: We use another Mobject or coordinate as a reference to locate our Mobject.
Mobject.to_edge()
Mobject.to_corner()
Mobject.next_to()
Mobject.align_to()
This method requires a three-dimensional array (coordinate) to locate an object on the screen, always use the center of the camera center as a reference.
Warning
Remember that if you place a Mobject outside the limits of the camera then your Mobject will not be visible in your animation, although Manim will have computed it.
def construct(self):
req = Rectangle()
req.move_to([-3,2,0])
self.add(req)
In general, it is advisable to convert your coordinates to np.array, or to use linear combinations of the one-dimensional or two-dimensional vectors to locate your objects.
def construct(self):
r = Rectangle()
c = Circle()
e = Ellipse()
# Best practice
r.move_to( np.array([-3, 2, 0]) )
# Other way
c.move_to( LEFT * 3 + UP * 2 )
# Another way
e.move_to( UL * 2 + LEFT )
self.add(r,c,e)
This method is similar to move_to, but the difference is that move_to always refers to the center of the camera (the origin), while shift refers to the current position of your Mobject.
To differentiate it, let’s look at the following case:
def construct(self):
s = Square()
c = Circle()
# Apply move_to 4 times
for _ in range(4):
s.move_to(RIGHT)
# Apply shift 4 times
for _ in range(4):
c.shift(RIGHT)
self.add(s,c)
If we apply the same move_to 4 times, then it is redundant, because the movement always takes the center of the camera as a reference.
But applying shift 4 times is different, because each shift takes the new Mobject coordinates as a reference.
This can be made even clearer with an animation:
def construct(self):
s = Square()
c = Circle()
self.add(s,c)
for _ in range(4):
# Pause
self.wait()
# Move
c.shift(RIGHT)
s.move_to(RIGHT)
You can notice that at the beginning both appear in the center of the camera, then the first cycle of the loop is applied and both move once to the right, but the second time only the circle (to whom the shift is applied) is it keeps moving, because the new shift (of the following loops) takes the new Mobject (circle) coordinates as a reference.
To obtain the coordinates of an object we can use the following getters:
def construct(self):
r = Rectangle()
self.add(r)
center = r.get_center()
right = r.get_right()
left = r.get_left()
top = r.get_top()
bottom = r.get_bottom()
up_right = r.get_corner(UR)
up_left = r.get_corner(UL)
down_right = r.get_corner(DR)
down_left = r.get_corner(DL)
for n,p in zip(
["C" ,"R" ,"L" ,"T","B" ,"UR" ,"UL" ,"DR" ,"DL"],
[center,right,left,top,bottom,up_right,up_left,down_right,down_left]
):
t = Text(f"{n}",color=RED)
t.move_to(p)
self.add(t)
Warning
It is important to note that .get_center() does not get the geometric center or center of mass of the Mobject. What .get_center() does is “create” an imaginary rectangle whose borders contain the entirety of your Mobject and then it returns the coordinates of that rectangle. If you need to obtain the center of mass of a Mobject use get_center_of_mass().
Also exists:
Mobject.get_x() # <==> Mobject.get_center()[0]
Mobject.get_y() # <==> Mobject.get_center()[1]
Mobject.get_z() # <==> Mobject.get_center()[2]
# N is some real number
Mobject.set_x(N) # <==> Mobject.move_to(RIGHT * N)
Mobject.set_y(N) # <==> Mobject.move_to(UP * N)
Mobject.set_z(N) # <==> Mobject.move_to(OUT * N)
This method moves vertically or horizontally to some edge of the camera, takes a one-dimensional vector as an argument and moves the Mobject in that direction to the edge.
Examples:
def construct(self):
req = Rectangle()
req.to_edge(LEFT)
self.add(req)
def construct(self):
req = Rectangle()
req.to_edge(UP)
self.add(req)
We can even use this method twice to move an object to a corner:
def construct(self):
req = Rectangle()
req.to_edge(UP)
req.to_edge(LEFT)
self.add(req)
This method admits a parameter called buff (buffer), this parameter indicates a gap between the border and your object, by default the value of this buffer is 0.5 units, but we can reduce this gap to zero using:
def construct(self):
req = Rectangle()
req.to_edge(UP)
req.to_edge(LEFT,buff=0)
self.add(req)
This method requires a two-dimensional vector, and places your Mobject in the corner. It is equivalent to using Mobject.to_edge() twice. It also supports the buff parameter.
def construct(self):
req = Rectangle()
req.to_corner(UL)
self.add(req)
def construct(self):
req = Rectangle()
req.to_corner(DR,buff=0)
self.add(req)
This method uses the edge of a Mobject/point and positions our Mobject in the direction of that edge, the format is as follows:
Mobject.next_to(REFERENCE_MOBJECT_OR_POINT, DIRECTION, buff=BUFFER, aligned_edge=EDGE)
Here we see some examples:
def construct(self):
# Reference Mobject:
rm = Rectangle()
# Mobjects that we want to move:
red_dot = Dot(color=RED)
blue_dot = Dot(color=BLUE)
green_dot = Dot(color=GREEN)
t = Text("Some text")
# Set positions
red_dot.next_to(rm, LEFT)
blue_dot.next_to(rm, LEFT, buff=0)
green_dot.next_to(rm, DR, buff=0)
t.next_to(rm, DOWN, aligned_edge=LEFT)
# -----------------
# Delete this parameter and see what
# happens, then change LEFT to RIGHT
self.add(
rm,
red_dot,
blue_dot,
green_dot,
t
)
You can notice that .next_to() will never move a Mobject to the center of another Mobject, it always takes as a reference the edge that you indicate in the second argument.
The parameter aligned_edge allows you to align your Mobject with the edge of the reference Mobject.
This is a somewhat complicated method to understand, but quite useful, its behavior is similar to what you saw with the aligned_edge parameter of .next_to().
def construct(self):
c = Circle()
c.move_to(RIGHT * 3 + UP * 1.5)
r = Rectangle()
r.align_to(c,RIGHT)
self.add(c,r)
Also works with corners:
def construct(self):
r = Rectangle()
r.move_to(RIGHT * 3 + UP * 1.5)
t = Text("Hello")
t.align_to(r,UR)
self.add(r,t)
Obviously, all Mobjects have height and width, additionally, three-dimensional Mobjects also have depth. To be able to modify them it is very simple:
def construct(self):
c = Circle()
r = Rectangle()
# replace "width" with "height
# and see what happens
c.width = 3
r.width = 3
self.add(c,r)
We can also pass the width from one Mobject to another like this:
def construct(self):
c = Circle()
r = Rectangle()
# replace "width" with "height
# and see what happens
c.width = r.width
# or
c.scale_to_fit_width(r.width)
# c.scale_to_fit_height(r.height)
self.add(c,r)
Another way to define the width or height is using the .set method:
def construct(self):
c = Circle()
r = Rectangle()
# replace "width" with "height
# and see what happens
c.set(width=3)
r.set(width=3)
self.add(c,r)
If you don’t want the proportions of your Mobject to be kept when changing the width or height then you can use .stretch_to_fit_height() or .stretch_to_fit_width():
def construct(self):
c = Circle()
t = Triangle()
r = Rectangle()
t.stretch_to_fit_height(c.height)
r.stretch_to_fit_width(c.width)
t.move_to(c.get_center()) # What happend if you remove this line
self.add(c,t,r)
def construct(self):
# Original circle
c_original = Circle(color=RED)
# x2
c_x_2 = Circle(color=WHITE)
c_x_2.scale(2)
# x3
c_x_3 = Circle(color=BLUE)
c_x_3.scale(3)
# x 1/3
c_x_1_3 = Circle(color=GREEN)
c_x_1_3.scale(1/3)
self.add(
c_original,
c_x_2,
c_x_3,
c_x_1_3
)
As its name indicates, it applies a linear transformation to a Mobject, you can use the following image as a reference. This is a generalization of all the properties previously seen.
def construct(self):
sq_phantom = Square()
sq = Square(color=RED)
ANGLE = PI / 6
# Reference point
POINT = sq.get_corner(DL)
matrix = [
[1,np.tan(ANGLE),0],
[0,1,0],
[0,0,0]
]
sq.apply_matrix(matrix,about_point=POINT)
self.add(sq_phantom, sq)
The previously studied properties work for any Mobject, now we will study the properties that only VMobjects have. The most common VMobjects are:
SVGMobject
Some Subclasses:
Text,Tex,MathTex,MarkupText, etc.
Geometry VMobjects:
Line,Arrow,Circle,Rectangle, etc.
ManimCE’s official documentation gives us all the default colors:
You must write them in capital letters.
Note
The colors of type “C” have an alias equal to the colorname without a letter, e.g.
GREEN = GREEN_C
You can define the color using the hexadecimal format, with RGB or with HSL:
def construct(self):
from colour import Color
def HSL(hue,saturation=1,lightness=0.5):
return Color(hsl=(hue/360,saturation,lightness))
red_dot = Dot(color=RED) .scale(4) .to_edge(UP)
blue_e_dot = Dot(color=BLUE_E) .scale(4) .to_edge(DOWN)
hex_dot = Dot(color="#FE298D") .scale(4) .to_edge(LEFT)
rgb_dot = Dot(color=rgb_to_color([0.2,0.9,0])).scale(4)
hsl_color = Dot(color=HSL(45,1,0.5)).scale(4) .to_edge(RIGHT)
self.add(
red_dot, blue_e_dot,
hex_dot, rgb_dot, hsl_color
)
def construct(self):
background_square = Square(
fill_opacity=1,
fill_color=WHITE,
)
background_square.scale(1.5)
circle = Circle(
# stroke options
stroke_width=20,
stroke_color=TEAL,
stroke_opacity=0.5, # 0 <= stroke_opacity <= 1
# fill options
fill_opacity=0.5, # 0 <= fill_opacity <= 1
fill_color=ORANGE
)
self.add(
background_square,
circle
)
A quick way to set a color is using .set_color(SOME_COLOR), in case you have already defined the color of the stroke width or the fill, both properties will take the color that you indicated in the .set_color() method.
def construct(self):
c = Circle(
stroke_color=PINK,
stroke_width=30,
stroke_opacity=0.4,
fill_opacity=0.6,
fill_color=ORANGE
)
c.set_color(RED)
self.add(c)
As we explained at the beginning, all VMobjects are bézier curves, and therefore have control points, you can visualize them quite easily:
def construct(self):
c = Circle()
# c.points are the control points
for p in c.points:
d = Dot().move_to(p)
self.add(d)
self.add(c)
You can even modify the points at runtime:
def construct(self):
c = Circle()
c.points[4] += LEFT
for p in c.points:
d = Dot().move_to(p)
self.add(d)
self.add(c)
In general, this type of manipulation is not useful, but with this we can obtain the starting and ending point of our path. In general, this type of manipulation is not useful, but thanks to this we can obtain the starting and ending point of our path. It is very useful when we use it with Line or similar.
def construct(self):
arrow = Arrow(LEFT,UR)
arrow.shift(LEFT+DOWN)
arrow_start = arrow.get_start() # same as arrow.points[0]
arrow_end = arrow.get_end() # same as arrow.points[-1]
dot_start = Dot(color=RED).move_to(arrow_start)
dot_end = Dot(color=BLUE).move_to(arrow_end)
self.add(arrow, dot_start, dot_end)
Sometimes it is convenient to copy an instance of a Mobject, to do this we can use the .copy() method.
def construct(self):
original_circle = Circle(
radius=2,
stroke_color=PINK,
stroke_width=30,
stroke_opacity=0.4,
fill_opacity=0.6,
fill_color=ORANGE
)
original_circle.to_edge(LEFT)
copy_circle = original_circle.copy()
copy_circle.to_edge(RIGHT)
# set_color
copy_circle.set_color(RED)
# set_stroke
copy_circle.set_stroke(color=TEAL,width=50,opacity=1)
# set_fill
copy_circle.set_fill(color=PURE_BLUE,opacity=1)
another_copy_circle = copy_circle.copy()
another_copy_circle.move_to(ORIGIN)
# set_style
another_copy_circle.set_style(
stroke_width=30,
stroke_color=WHITE,
stroke_opacity=0.5,
fill_color=PURE_GREEN,
fill_opacity=0.3,
)
As you can see, it is possible to change the thickness and padding properties after the instance, either by using .set_stroke(), .set_fill(), or by using .set_style().
Note
The .copy() method works with any Mobject, not just VMobjects.
Create a grid in which you can see the coordinates of the screen:
Draw the Yin-Yang symbol.
Draw the VUE.js logo.
As we explained in previous chapters, the camera has an aspect ratio of 16/9, it has a black background and there are several default rendering options (l, m, h and k). In this chapter we are going to extend this knowledge.
There are three ways to configure the camera and rendering options:
Modifying the config dictionary inside a .py script.
Using the ManimCE CLI.
Using a manim.cfg file.
manim.cfg files do not always work very well, some properties work and others do not, at the moment I do not recommend using it. But if you want to learn how to use it, just type the command:
manim cfg write -o
This command will create a configuration file with all the options that you can modify. As I said before, some options may not work, it is still under development.
Read the documentation here [https://docs.manim.community/en/stable/tutorials/configuration.html#the-config-files].
Unfortunately the CLI is constantly changing, some functionalities may change as the versions progress, so I will leave the options that are more stable.
PREVIEW: -p: Automatically open your file at the end of the rendering is done, be it a video or an image.
QUALITY: -ql: 420p at 15FPS, -qm: 720p at 30FPS, -qh: 1080p at 60FPS, -qp: 1440p at 60FPS and -qk: 4K at 60FPS.
RENDER LAST FRAME AS IMAGE: -s.
RENDER AS GIF: --format=gif.
RENDER AS MOV: --format=mov - This format is the one with the best quality.
SET FPS: --fps=SOME_NUMBER.
WITH TRANSPARENCY: -t - If you use this flag while rendering a video then it will be exported in .mov format in an alpha channel, in case you render an image it will export a PNG without background.
CHANGING ASPECT RATIO: -r W,H - For example: -r 1000,500. I do not recommend doing this unless you have a more advanced understanding of the camera.
CHANGE OUTPUT FILENAME: -o FILE_NAME.
START RENDERING FROM…: -n START,END - It only works in video rendering, it starts rendering from START animation to END animation, where START and END are the numbers of some animations, START < END.
RENDER ALL SCENES FROM FILE: -a - If you have more than one class that inherits from Scene (or its variants) then it will render all these scenes.
Render a scene at 720p, no preview, at 15 fps as a gif and with the output name of MY_SCENE:
manim scripy.py SceneName -qm --fps=15 --format=gif -o MY_SCENE
Render a scene with 800 pixels width x 700 pixels HEIGHT, at 20 fps, output name = OTHER_SCENE, starting at animation 3 and ending at animation 20, with preveiw and transparency.
manim scripy.py SceneName -p --fps=20 -r 800,700 -t -o OTHER_SCENE -n 3,20
config dictWe can define almost all the options in our same .py script that we are working on without having to alter the CLI or use .cfg files.
from manim import *
config.background_color = RED
config.frame_rate = 25
config.frame_size = [1200,500] # [WIDTH, HEIGHT]
# config.transparent = True
# RENDER AS IMAGE: ----------------
config.format = "png"
config.save_last_frame = True
# config.transparent = True
# REDER AS GIF: -------------------
config.format = "gif"
class SomeScene(Scene):
...
Now, in case you want to use several of these settings at the same time, you should know that Manim is going to take the following priority:
config dict.
ManimCE CLI.
manim.cfg
That is, the configuration with the highest priority will be the config dictionary within your script, if you have not modified it then it will take the CLI, and if you have not configured the CLI then it will read your manim.cfg, and in case of If you haven’t configured manim.cfg then Manim will use the default settings.
There are two factors that Manim takes into account when superimposing one Mobject on top of another when rendering a scene:
The order in which the Mobjects are added to the scene using Scene.add or Scene.play.
The z_index.
Scene.mobjectsWhen you add a Mobject to the scene (either with Scene.add or Scene.play), Manim adds that Mobject to a list called Scene.mobjects in the order in which they are added in the code, and by default, to all Mobjects they are assigned a z_index = 0.
See this code:
def construct(self):
mob_kwargs = {"fill_opacity": 1}
c_red = Circle(color=RED,**mob_kwargs).shift(LEFT)
t_teal = Triangle(color=TEAL,**mob_kwargs)
s_purple = Square(color=PURPLE,**mob_kwargs).shift(RIGHT)
[mob.scale(2) for mob in [c_red, t_teal, s_purple]]
self.add(c_red,t_teal) # L1
self.add(s_purple) # L2
# Print list
print(self.mobjects)
OUT:
[Circle, Triangle, Square]
If you play with the commented lines with L1 and L2 changing the order of how you add the Mobjects you will see that the list and that the Mobjects in the camera change.
Specifically, the first is to be added to the bottom, the second overlaps the first, and the third overlaps the first two, and so on.
z_indexSometimes it is difficult to control the order of the Mobjects in the Scene.mobjects list, so the z_index functionality was added.
By default, all Mobjects have z_index = 0, but you can change this using Mobject.set_z_index(INDEX).
Play with the following code:
def construct(self):
mob_kwargs = {"fill_opacity":1,"radius":2.5}
mob_teal = Circle(color=TEAL,**mob_kwargs)\
.shift(LEFT)
mob_purple = Circle(color=PURPLE,**mob_kwargs)\
.shift(LEFT*2)
mob_red = Circle(color=RED,**mob_kwargs)
mob_yellow = Circle(color=YELLOW,**mob_kwargs)\
.shift(RIGHT)
mob_orange = Circle(color=ORANGE,**mob_kwargs)\
.shift(RIGHT*2)
mob_purple.set_z_index(0)
mob_teal .set_z_index(1)
mob_red .set_z_index(2)
mob_yellow.set_z_index(3)
mob_orange.set_z_index(4)
self.add(mob_purple, mob_teal,mob_red,mob_yellow,mob_orange)
And the best thing is that these positions are fulfilled in the animations and in 3D scenes.
If two Mobjects have the same z_index then the Scene.mobjects list rule will apply.
Rate functions are, as their name implies, functions, normalized functions, whose domain goes from 0 to 1 and the range must be within the interval \([0,1]\), but nothing prevents it from starting or ending in those limits.
\(0\) means \(0\%\) of run_time and \(1\) means \(100\%\) of run_time, and run_time is the total duration of the animation.
Some rate functions that are already predefined are explained below.
The following animation explains how the rate functions work. Each animation starts from a state of \(0\%\) complete to \(100\%\) complete, the rate functions change the behavior of that increment.
These functions are used to change the way class animations work.
Each class animation has a predefined rate function, for example, the Write animation has linear as the default rate_func. But they are easy to change.
If we play around with the behavior of the function we can do something like this:
def construct(self):
text = Text("Smooth")
text.set(width=config.frame_width-1)
self.play(
Write(text,rate_func=smooth)
)
self.wait()
self.play(
# inverse smooth function
Write(text,rate_func=lambda t: smooth(1-t))
)
self.wait()
Inside the Scene.play method you can specify a rate_func and run_time for all animations, or you can specify each rate_func and run_time for each animation.
# For all Animations
self.play(
Animation1(...),
Animation2(...),
Animation3(...),
....
AnimationN(...),
rate_func=some_rate_func,
run_time=some_run_time,
)
# For each animation
self.play(
Animation1(...,rate_func=rf1,run_time=rt1),
Animation2(...,rate_func=rf2,run_time=rt2),
Animation3(...,rate_func=rf3,run_time=rt3),
....
AnimationN(...,rate_func=rfN,run_time=rtN),
)
In short, rate_func is the behavior of the animation, and run_time is the duration of the animation. And they can be defined for each (class) animation or for all animations within a Scene.play.
To understand how assets are imported into Manim, you first have to set a workspace, that is, a folder.
It is advisable to create a folder for assets and each type of document, for example:
.
├── assets
│ ├── images
│ ├── sounds
│ └── svg
│
└── your_script.py
Manim supports PNG images (opaque or with transparency), JPEG, JPG and SVG. However, SVGs must be well built, that is, they must not have errors in their code for it to work.
Download the following files:
Normal raster image: test_image.
PNG with transparency: test_transparency.
SVG: svg_test.
And locate them as follows:
├── assets
│ ├── images
│ │ ├── test_image.png
│ │ └── test_transparency.png
│ │
│ └── svg
│ └── svg_test.svg
│
└── your_script.py
The way to import it is as follows:
def construct(self):
# Change background color only this scene
self.camera.background_color = TEAL
test_image = ImageMobject("assets/images/test_image.png")
test_image.to_corner(LEFT)
test_image.set(width=3)
test_transparency = ImageMobject("assets/images/test_transparency.png")
test_transparency.set(width=3)
svg = SVGMobject("assets/svg/svg_test")
svg.set(width=3)
svg.to_edge(RIGHT)
self.add(
test_image,
test_transparency,
svg
)
self.wait()
Note
Note that ImageMobject and SVGMobject refer to the directory where you are running Manim (your workspace).
Warning
Class animations that were designed for VMobjects cannot be applied to ImageMobjects. ImageMobjects are not VMobjects. For example, Write cannot be applied to an ImageMobject, but FadeIn or FadeOut can.
The same logic is applied as the images, it is recommended to create a folder called sounds, here is an example, download the following files and locate them in this way:
├── assets
│ └── sounds
│ ├── count.wav
│ └── finish.wav
│
└── your_script.py
Sound 1: count.wav.
Sound 2: finish.wav.
Code:
def construct(self):
for i in range(5):
t = Text(f"{i+1}")
t.set(height=config.frame_height - 2)
self.add(t)
if i != 4:
# "gain" is the amplification of the sound
self.add_sound("assets/sounds/count.wav",gain=3)
else:
self.add_sound("assets/sounds/finish.wav",gain=3)
self.wait()
self.remove(t)
Warning
It is recommended to use the --flush_cache flag, as sometimes the cache can cause the rendering to not work quite well, example:
manim your_script.py SomeScene -pqm --flush_cache
Sometimes it is very necessary to group several Mobjects to apply some Manim method to them.
For example, suppose we want to apply a shift to several Mobjects, it is true that we can group them in an array and make a loop, but it is much faster to group them in a Group and apply the shift to that Group.
The difference between Group and VGroup is simple, Group supports any Mobject, such as ImageMobject and VMobjects, but VGroup only supports VMobjects.
Group support any method of Mobjects, such as shift, scale, move_to, next_to, align_to, etc. However you cannot apply VMobjects methods to them, such as set_stroke or set_fill, if you want to use those methods then you will have to use a VGroup.
Two additional methods that are very useful are arrange and arrange_in_grid, they work quite simply:
It should be noted that when we apply arrange or arrange_in_grid, all Mobjects move to the center of the screen.
They are exactly the same as Groups, but you can apply the VMobjects methods, such as set_stroke, set_fill, set_style, etc.
def construct(self):
grp = VGroup(
Rectangle(), Circle(), Triangle(),
Text("A"),Text("B"),Text("C"),
)
grp.set_style(
fill_opacity=0,
stroke_width=4,
stroke_color=PURPLE
)
grp.arrange_in_grid(cols=3)
grp.width = config.frame_width - 1
self.add(grp)
Another advantage is that you can add, select and remove elements as you would with an array.
To add elements use Group.add(some_mobject)
To remove elements, use Group.remove(some_mobject), with the indication that some_mobject must already be included in the Group.
To select an item use Group[i] where i is a number, or a range, like Group[3:5]. If you use range then the return is another Group with those elements.
This technique is quite useful when we need to create Groups more quickly.
def construct(self):
from itertools import cycle
colors = cycle([RED,TEAL,ORANGE,PINK])
grp = VGroup(*[
Text(n,color=next(colors))
.scale(4)
for n in "ManimCE"
])
grp.arrange(RIGHT,aligned_edge=DOWN)
self.add(grp)
List comprehensions are quite useful in Manim, we will use them later in other chapters.
There are three ways to create texts in Manim:
With system fonts: Text.
With PangoMarkup: MarkupText.
With LaTeX: Tex and MathTex.
The simplest is with Text, so we’ll start with that.
Text is a subclass of SVGMobject. The text is rendered using Pango and uses the fonts you have installed.
To see all the fonts you can use the command:
python -c "import manimpango; from pprint import pprint; pprint(manimpango.list_fonts())"
It will give you an output like this:
['Academy Engraved LET',
'Al Bayan',
'Al Nile',
'Al Tarikh',
'American Typewriter',
'Andale Mono',
'Apple Braille',
'Apple Chancery',
'Apple Color Emoji',
'Apple SD Gothic Neo',
'Apple Symbols',
'AppleGothic',
'AppleMyungjo',
....
....
]
If you use a shell like BASH (or similar) you can do something like this:
python -c "import manimpango; from pprint import pprint; pprint(manimpango.list_fonts())" | grep -i arial
To filter your search.
Since it is an SVGMobject, then you can use all the methods of the VMobjects, i.e. set the stroke_width, fill_opacity, etc. By default the stroke is 0, but you can change that yourself.
def construct(self):
t = Text("Hello world")
self.add(t)
I invite you to change the color, stroke width, fill color, stroke color, etc. Either using methods after the instance, or at the time of the instance.
To indicate the font we want to use, we can indicate it in the “font” parameter:
def construct(self):
t = Text("Hello world", font="Arial")
self.add(t)
Text can be understood as SVG arrays, and thanks to this you can select each letter:
def construct(self):
t = Text("Hello world", font="Arial")
t[0].set_color(RED)
t[3].set_color(TEAL)
self.add(t)
If the text is long enough you can use triple quotes:
def construct(self):
t = Text(
"""
Lorem Ipsum is simply dummy text of the printing and typesetting industry.
Lorem Ipsum has been the industry's standard dummy text ever since the 1500s,
when an unknown printer took a galley of type and scrambled it to make a
type specimen book.
""",
line_spacing=1.3 # space between lines
)
t.width = config.frame_width - 1
t[0].set_color(RED)
self.add(t)
But if you want to do it in a single line, you can do it using \n:
def construct(self):
t = Text(
"Lorem Ipsum is simply\ndummy text of the printing",
line_spacing=1.3 # space between lines
)
t.width = config.frame_width - 1
self.add(t)
Slant is the style of the Text, and it can be NORMAL (the default), ITALIC or OBLIQUE. Usually, for many fonts both ITALIC and OBLIQUE look similar, but ITALIC uses Roman Style, whereas OBLIQUE uses Italic Style.
def construct(self):
t = Text(
"Lorem Ipsum is simply\ndummy text of the printing",
line_spacing=1.3, # space between lines
slant=ITALIC
)
t.width = config.frame_width - 1
self.add(t)
The different types of weight can be obtained from ManimPango:
def construct(self):
import manimpango as mp
weight_list = dict(
sorted(
{
weight: mp.Weight(weight).value
for weight in mp.Weight
}.items(), key=lambda x: x[1])
)
grp = VGroup(*[
Text(weight.name, weight=weight.name, font="Open Sans")
for weight in weight_list
]).arrange(DOWN)
grp.height = config.frame_height - 1
self.add(grp)
There are some functions that allow us to manipulate sections of text in a simple way, however, if there are repetitions of text, these functions may not behave correctly.
def construct(self):
grp = VGroup(
# Text to color
Text("Hello",t2c={"[1:-1]": BLUE}),
Text("World",t2c={"rl": RED}),
# Text to font
Text("Manim",t2f={"an": "Open Sans"}),
Text("Manim",t2f={"[2:-1]": "Open Sans"}),
# Text to gradient
Text("Hello",t2g={"[1:-1]": (RED,GREEN)}),
Text("World",t2g={"World": (RED,BLUE)}),
# Text to slant
Text("Manim",t2s={"an": ITALIC}),
Text("Manim",t2s={"[2:-1]": ITALIC}),
# Text to weight
Text("Manim",t2w={"an": THIN}, font="Open Sans"),
Text("Manim",t2w={"[2:]": HEAVY}, font="Open Sans"),
# Ligature
Text("fl ligature",font_size=40),
Text("fl ligature", disable_ligatures=True, font_size=40),
).arrange_in_grid(cols=2).scale(1.4)
self.add(grp)
MarkupText is a class that uses PangoMarkup, language similar to HTML that allows us to modify the text with XML tags, if you understand HTML or **XML then it will be easy for you to understand this class.
def construct(self):
text = MarkupText(
f'Normal <i>Italic</i> <b>Bold</b> <u>Underline</u> <span foreground="{BLUE}">Blue text</span>'
)
self.add(text)
I recommend reading the official documentation [https://docs.manim.community/en/stable/reference/manim.mobject.svg.text_mobject.MarkupText.html#manim.mobject.svg.text_mobject.MarkupText] if you want to learn how to use this tool as it has many configurations.
This is the most complex class, not only because it is the one with the most options, but because LaTeX is a world of its own.
If you do not have knowledge of LaTeX I recommend that you watch a 1-2 hour tutorial on the internet.
As you should know, to use LaTeX you must include the libraries you want to use, by default, Manim uses this template:
\usepackage[english]{babel}
\usepackage{amsmath}
\usepackage{amssymb}
But you can add, remove or modify predefined templates, you can see the predefined templates here [https://github.com/ManimCommunity/manim/blob/8c9ae5bb2a7d06a110e6871cd29339e8b2fe05ca/manim/utils/tex_templates.py#L81].
If you want to do it manually you must follow these steps:
# Create a new TexTemplate
my_template = TexTemplate()
# Add new preambles (can be more)
my_template.add_to_preamble(r"\newcommand{\st}[2]{{\tt S}_{\rm #1}^{\rm #2}}")
# Create a new subclass with your template
class MyTex(Tex):
def __init__(self, *args, **kwargs):
super().__init__(*args, tex_template=my_template, **kwargs)
class TestMyTexTemplate(Scene):
def construct(self):
text = MyTex("$\st{sub-index}{super-index}$").scale(3)
self.add(text)
What Manim renders is the following:
\documentclass[preview]{standalone}
\usepackage[english]{babel}
\usepackage{amsmath}
\usepackage{amssymb}
\newcommand{\st}[2]{{\tt S}_{\rm #1}^{\rm #2}}
\begin{document}
\begin{center}
$\st{sub-index}{super-index}$
\end{center}
\end{document}
You can add as many preambles as you want.
Also, if you want, you can create enviroments, I use a lot an enviroment that allows me to control the width of the justified paragraph.
class MyTex(Tex):
def __init__(self, *args, j_width=4, **kwargs):
super().__init__(*args, tex_environment="\\begin{tabular}{p{%s cm}}"%j_width, **kwargs)
class TestEnviroment(Scene):
def construct(self):
TEX = "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s"
t1 = MyTex(TEX)
t2 = MyTex(TEX, j_width=6)
t3 = MyTex(TEX, j_width=9)
grp = VGroup(t1,t2,t3).arrange(DOWN)
grp.height = config.frame_height - 1
self.add(grp)
You can find all the exported files in media/Tex.
Like Text, Tex is an array of several SVGs, but this one is a bit more complex.
Tex not only accepts a single string, you can add as many strings as you want:
def construct(self):
t1 = Tex("Hello world!")
# | Note the space
# v
t2 = Tex("Hello ","world","!")
grp = VGroup(t1,t2).arrange(DOWN,aligned_edge=LEFT)
self.add(grp)
This means that if we want to select only one element of Tex there are two possibilities.
If we want to select the third “l” of both texts then we must understand the following:
# Tex("Hello world!")
# returns a container, and that container
# contains another container with the letters,
# we can see it as:
# [['H','e','l','l','o','w','o','r','l','d','!']] (Blanks don't count in Tex)
# So, if you want to select the 3rd "l" you must do:
def construct(self):
t = Tex("Hello world!")
t[0][-3].set_color(RED)
self.add(t)
# Tex("Hello ","world","!")
# returns a container with 3 containers:
# [['H','e','l','l','o'],['w','o','r','l','d'],['!']]
# So, if you want to select the 3rd "l" you must do:
def construct(self):
t = Tex("Hello ","world","!")
t[1][-2].set_color(RED)
self.add(t)
Note
This topic will be important when we do formula transformations.
This is equivalent to t2c, it works well if your text is simple, but if you use enviroments or your Tex is very complex, it will most likely not work, so you should use it with discretion.
def construct(self):
t = Tex(
"Hello my ","world",
tex_to_color_map={
"Hello": RED,
"wor": ORANGE
}
)
self.add(t)
If Tex is LaTeX in normal mode, then MathTex is LaTeX in math mode, that is, MathTex("formula") is equivalent to Tex("$$formula$$").
The transformations are classified into two:
Transform
ReplacementTransform
All other animations in this style are derived from these two.
To understand the transformations, we must first understand that each instance of a Mobject has a special identifier that differentiates it from the others, thus, two instances of the same Mobject will have different identifiers.
def construct(self):
d1 = Dot()
d2 = Dot()
d3 = d1
d4 = d2.copy()
print(f"id(d1) = f{id(d1)}")
print(f"id(d2) = f{id(d2)}")
print(f"id(d3) = f{id(d3)}")
print(f"id(d4) = f{id(d4)}")
Results (The values change on every computer or session):
id(d1) = f5547969504
id(d2) = f5548828560
id(d3) = f5547969504
id(d4) = f5534488176
You can see that even though d1 and d2 are instances of the same Mobject (Dot) but they have different id. But, d1 and d3 have the same id, that means, that both values point to the same place in memory, so it is the same to modify d1 or d3, since it is exactly the same object, only with a different name. d4 is an object created from d2, but they are not the same object.
This is not something special from Manim, all objects are like that.
Something similar happens in transformations.
Transform needs two and only two Mobjects, the object to transform and the target.
Transform copies the properties of the target and passes them to the object to be transformed, but does not modify the target, it only modifies the object to transform.
def construct(self):
obj = Text("X")
t_a = Text("A")
t_b = Text("B")
t_c = Text("C")
t_d = Text("D")
self.add(obj)
self.play(Transform(obj,t_a))
self.play(Transform(obj,t_b))
self.play(Transform(obj,t_c))
self.play(Transform(obj,t_d))
self.wait()
t_grp = VGroup(t_a,t_b,t_c,t_d)\
.arrange(DOWN)\
.shift(RIGHT)
self.play(Write(t_grp))
self.wait()
It is easy to notice here that neither t_a, t_b, t_c nor t_d changed their value, the only Mobject that changed was obj.
It is equivalent to Transform, with the detail that ReplacementTransform does change the content of the target.
def construct(self):
obj = Text("X")
t_a = Text("A")
t_b = Text("B")
t_c = Text("C")
t_d = Text("D")
self.add(obj)
self.play(ReplacementTransform(obj,t_a))
# self.play(ReplacementTransform(obj,t_b)) # <- This not works
self.play(ReplacementTransform(t_a,t_b))
self.play(ReplacementTransform(t_b,t_c))
self.play(ReplacementTransform(t_c,t_d))
self.wait()
t_grp = VGroup(obj,t_a,t_b,t_c)\
.arrange(DOWN)\
.shift(RIGHT)
self.play(Write(t_grp))
self.wait()
You can notice that, while in Transform we always use the same object in the first argument, ReplacementTransform does change its first argument.
It is equivalent to Transform, only that instead of interpolating bézier curves, it uses a FadeIn and FadeOut to the Mobjects to transform:
def construct(self):
r = Rectangle()
c = Circle()
VGroup(r,c).arrange(RIGHT)
self.add(r,c)
self.play(
FadeTransform(r,c)
)
self.wait()
This animation is especially useful when we transform formulas or text that does not have the same style. For example, if we use transform in this animation:
def construct(self):
t1 = MathTex("e^","\\frac{-it\\pi}{\\omega}")
t2 = MathTex("\\frac{-it\\pi}{\\omega}")
VGroup(t1,t2)\
.scale(3)\
.arrange(DOWN,buff=2)
self.add(t1,t2.copy().fade(0.8))
self.wait(0.3)
self.play(
ReplacementTransform(t1[-1].copy(),t2[0]),
run_time=6
)
self.wait()
You can see that the transformation does not look good, this is because, although it is the same text, the letters have slightly different sizes and shapes, for this we can use FadeTransform, in this case we will use FadeTransformPieces, which allows us to transform each submobject (Mobjects inside other Mobjects) using FadeTransform. That is, FadeTransformPieces will transform each letter using FadeTransform. If we used FadeTransform then it would apply to the entire formula.
def construct(self):
t1 = MathTex("e^","\\frac{-it\\pi}{\\omega}")
t2 = MathTex("\\frac{-it\\pi}{\\omega}")
VGroup(t1,t2)\
.scale(3)\
.arrange(DOWN,buff=2)
self.add(t1,t2.copy().fade(0.8))
self.wait(0.3)
self.play(
FadeTransformPieces(t1[-1].copy(),t2[0]),
run_time=4
)
self.wait()
This animation try to transform groups by matching the shape of their submobjects.
Two submobjects match if the hash of their point coordinates after normalization (i.e., after translation to the origin, fixing the submobject height at 1 unit, and rounding the coordinates to three decimal places) matches.
def construct(self):
from random import shuffle
def get_mobs():
mob = [Square(),Circle(),Triangle(),Text("Hello")]
shuffle(mob)
return mob
grp1 = VGroup(*get_mobs()).arrange(DOWN)
grp2 = VGroup(*get_mobs()).arrange(DOWN)
VGroup(grp1,grp2).arrange(RIGHT,buff=4)
self.add(grp1,grp2)
self.play(
TransformMatchingShapes(
grp1.copy(), grp2
)
)
self.wait()
Other example:
def construct(self):
source = Tex("the morse code", height=1)
target = Tex("here come dots", height=1)
self.add(source)
self.wait()
kw = {"run_time": 3, "path_arc": PI / 2}
self.play(TransformMatchingShapes(source, target, **kw))
self.wait()
self.play(TransformMatchingShapes(target, source, **kw))
self.wait()
It is equivalent to the previous transformation, and what it does is transform each tex_string. It is recommended to separate each tex_string that you want to isolate for the transformation, there are two ways, using an array with the strings to isolate.
def construct(self):
isolate_tex = ["x","y","3","="]
t1 = MathTex("x+y=3",substrings_to_isolate=isolate_tex)
t2 = MathTex("x=3-y",substrings_to_isolate=isolate_tex)
VGroup(t1,t2)\
.scale(3)
t2.align_to(t1,LEFT)
self.add(t1)
self.wait()
self.play(
TransformMatchingTex(
t1,t2,
# Try removing this dict
key_map={
"+":"-"
}
),
run_time=4
)
self.wait()
Or using this format:
def construct(self):
t1 = MathTex("{{x}}+{{y}}={{4}}")
t2 = MathTex("{{x}}={{4}}-{{y}}")
VGroup(t1,t2)\
.scale(3)
t2.align_to(t1,LEFT)
self.add(t1)
self.wait()
self.play(
TransformMatchingTex(
t1,t2,
# Try removing this dictionary
key_map={
"+":"-"
}
),
run_time=4
)
self.wait()
Use the key_map dictionary to specify symbols that you want to transform into others.
In general, this transformation works well for simple formulas, but for more complex formulas we will need to use indexes.
This fail with a little complex formulas (roots, fractions, etc):
def construct(self):
isolate_tex = ["a","b","c","="]
t1 = MathTex("a \\times b = c",substrings_to_isolate=isolate_tex)
t2 = MathTex("a = { c \\over b }",substrings_to_isolate=isolate_tex)
VGroup(t1,t2)\
.scale(3)
t2.align_to(t1,LEFT)
self.add(t1)
self.wait()
self.play(
TransformMatchingTex(
t1,t2,
# This not works
key_map={
"\\times":"\\over"
}
),
run_time=4
)
self.wait()
Sometimes the formulas we want to transform are very complex, so TransformMatchingShapes or TransformMatchingTex won’t work, so the only alternative is to use Transform with subindexes.
For this, we need to identify each index of each formula, we can do that using Manim itself.
Using this function:
def get_sub_indexes(tex):
ni = VGroup()
colors = cycle([RED,TEAL,GREEN,BLUE,PURPLE])
for i in range(len(tex)):
n = Text(f"{i}",color=next(colors)).scale(0.7)
n.next_to(tex[i],DOWN,buff=0.01)
ni.add(n)
return ni
We can identify the subindexes of each formular, for example:
def construct(self):
# Why this? |
# v
source = MathTex("\\sqrt{\\frac{1}{8}}")[0]
target = MathTex("\\frac{1}{2\\sqrt{2}}")[0]
# If you ask yourself this, go back to "Tex as array"
# section in the "Text and Tex" chapter
VGroup(source,target).scale(4).arrange(RIGHT,buff=2)
source_ind = get_sub_indexes(source)
target_ind = get_sub_indexes(target)
self.add(
source, source_ind,
target, target_ind
)
Now all we have to do is relate them.
0 [root v] --> 3 [root v]
1 [root top] --> 4 [root top]
2 [1] --> 0 [1]
3 [fraq line] --> 1 [fraq line]
4 [8] --> 2 [2 left], 5 [2 right]
We can do it in many ways, one of those could be:
def construct(self):
source = MathTex("\\sqrt{\\frac{1}{8}}")[0]
target = MathTex("\\frac{1}{2\\sqrt{2}}")[0]
VGroup(source,target).scale(4)
self.add(source)
transform_index = [
[0,1,2,3,4,4],
# | | | | | | < Note that we repeat the index 4 twice,
# v v v v v v since the "8" is going to transform
[3,4,0,1,2,5] # into two different symbols.
]
self.play(
*[
ReplacementTransform(source[i],target[j])
for i,j in zip(*transform_index)
]
)
self.wait()
We notice that the 8 does not transform well, because it is transforming into 2 different symbols, what we can do is duplicate it using a copy, a simple way would be:
def construct(self):
source = MathTex("\\sqrt{\\frac{1}{8}}")[0]
target = MathTex("\\frac{1}{2\\sqrt{2}}")[0]
VGroup(source,target).scale(4)
self.add(source)
transform_index = [
[0,1,2,3,4,"r4"],
# | | | | | |
# v v v v v v
[3,4,0,1,2, 5]
]
self.play(
*[
# Try replacing "ReplacementTransform" with "FadeTransform"
ReplacementTransform(source[i],target[j])
if type(i) is int else
ReplacementTransform(source[int(i[1:])].copy(),target[j])
for i,j in zip(*transform_index)
],
run_time=3
)
self.wait()
Following the same example we can continue adding conditionals to do things like this:
def construct(self):
source = MathTex("\\sqrt{\\frac{1}{8}}")[0]
target = MathTex("\\frac{1}{2\\sqrt{2}}")[0]
VGroup(source,target).scale(4)
self.add(source)
transform_index = [
["f0","f1",2,3,4,"r4"],
# | | | | | |
# v v v v v v
[ 3, 4, 0,1,2, 5]
]
self.play(
*[
ReplacementTransform(source[i],target[j])
if type(i) is int else
ReplacementTransform(source[int(i[1:])].copy(),target[j])
if i[0]=="r" else
FadeTransform(source[int(i[1:])],target[j])
for i,j in zip(*transform_index)
],
run_time=3
)
self.wait()
In a later workshop we will teach a method to make this process simpler.
Once we have understood how the transformations work, we can enter other functionalities based on them. Everything that we will see next is derived from the transformations.
This is quite a useful animation. Basically it is about creating a temporary copy of a Mobject called target, then making modifications to that copy (target) to end with a transformation from the original Mobject to the target.
We could do this manually, in the following example we will see MoveToTarget on the left side and on the right side we will see its analog manually.
def construct(self):
source_left = Dot()
source_right = source_left.copy()
VGroup(source_left,source_right).arrange(RIGHT,buff=3)
# Left side - MoveToTarget ----------------
source_left.generate_target()
# Manupulate the .target attr
source_left.target.set_style(
fill_color=TEAL,
stroke_width=10,
stroke_color=ORANGE
)
source_left.target.scale(7)
source_left.target.to_edge(UP)
# Right side - Manually ----------------
source_right_target = source_right.copy()
source_right_target.set_style(
fill_color=TEAL,
stroke_width=10,
stroke_color=ORANGE
)
source_right_target.scale(7)
source_right_target.to_edge(UP)
# Animations
self.add(source_left,source_right)
self.play(
MoveToTarget(source_left),
Transform(source_right,source_right_target),
run_time=3
)
self.wait()
It may not sound like a big deal, and it certainly isn’t, but using this technique saves you from creating new variables that you have to remember the name of. It is a good idea to use it on certain occasions.
This is another very useful animation, basically it allows you to enter a function that is going to be applied to some Mobject.
def construct(self):
source = Dot()
def custom_func(mob):
mob.set_style(
fill_color=TEAL,
stroke_width=10,
stroke_color=ORANGE
)
mob.scale(7)
mob.to_edge(UP)
# Don't forget return mob
return mob
self.add(source)
self.play(
ApplyFunction(custom_func,source),
run_time=3
)
self.wait()
But not only that, this can be applied to several Mobjects at the same time, making this animation reusable:
def construct(self):
source = VGroup(Dot(),Square(),Circle(),Text("A"))\
.arrange(RIGHT,buff=2)
def custom_func(mob):
mob.set_style(
fill_color=TEAL,
stroke_width=10,
stroke_color=ORANGE
)
mob.scale(3)
mob.to_edge(UP)
# Don't forget return mob
return mob
self.add(source)
self.play(
*[
ApplyFunction(custom_func, mob)
for mob in source
],
run_time=3
)
self.wait()
Wait, there is more, using the concept of closures, we can do more interesting things, instead of having a static function we can create a function that generates other functions, and reuse it even more flexibly.
In Manim, these types of closures are often created as separate methods from construct.
class MyScene(Scene):
def construct(self):
source = VGroup(Dot(),Square(),Circle(),Text("A"))\
.arrange(RIGHT,buff=2)
self.add(source)
self.play(
ApplyFunction(self.custom_method(scale=7,edge=DOWN), source[0]), # Dot
ApplyFunction(self.custom_method(fill_color=PURPLE), source[1]), # Square
ApplyFunction(self.custom_method(fill_opacity=0), source[2]), # Circle
ApplyFunction(self.custom_method(edge=LEFT), source[3]), # Text("A")
run_time=3
)
self.wait()
def custom_method(self,
fill_color=TEAL,
fill_opacity=1,
stroke_width=10,
stroke_color=ORANGE,
scale=3,
edge=UP,
):
def custom_func(mob):
mob.set_style(
fill_color=fill_color,
fill_opacity=fill_opacity,
stroke_width=stroke_width,
stroke_color=stroke_color,
)
mob.scale(scale)
mob.to_edge(edge)
# Don't forget return mob
return mob
# Don't forget return the func
return custom_func
Another functionality of ApplyFunction is the manipulation of VGroups or Groups, since it allows manipulating each element of the VGroups/Groups if we need it.
def construct(self):
source = VGroup(Dot(),Square(),Circle(),Text("A"))\
.arrange(DOWN)
def grp_func(vgrp):
vgrp.scale(2)
d,s,*other = vgrp
# The order is important
d.scale(4)
s.set_color(PURPLE)
vgrp.arrange(RIGHT,buff=0)
VGroup(*other).set_color(TEAL)
return vgrp
self.add(source)
self.play(
ApplyFunction(grp_func, source),
run_time=3
)
self.wait()
.animateThis functionality is possibly the most versatile. It basically allows us to animate almost any interpolatable method, and its use is quite simple:
def construct(self):
source = VGroup(Dot(),Square(),Circle(),Tex("Hello ","World","!"))\
.arrange(RIGHT,buff=1).to_edge(DOWN)
d,s,c,t = source
self.add(source)
self.play(
# Single method
d.animate.to_edge(UP),
# Multi-methods single line
s.animate.shift(UP*2).scale(1/2).set_color(PURPLE),
# Multi-methods multi lime
c.animate
.set_x(-1)
.scale(1/2)
.set_style(fill_opacity=1,fill_color=PINK,stroke_color=WHITE),
# Even VGroups or Groups
t.animate.arrange(DOWN,aligned_edge=LEFT).move_to(t.get_center()+UP*3),
# -----------------------------
# Remember that the arrange method moves the Mobjects to the center
# so we can't use "shift" method here.
run_time=3
)
self.wait()
We can think of .animate as a quick equivalent to MoveToTarget or ApplyFunction:
def construct(self):
vgrp = VGroup(Square(),Square(),Square())\
.arrange(DOWN).to_edge(LEFT)
s1, s2, s3 = vgrp
SCALE = 0.3
s2.generate_target()
s2.target.scale(SCALE)\
.set_fill(opacity=1)\
.set_color(TEAL)\
.to_edge(RIGHT)
self.add(vgrp)
self.play(
# .animate ----------------------------
s1.animate
.scale(SCALE)
.set_fill(opacity=1)
.set_color(TEAL)
.to_edge(RIGHT),
# MoveToTarget ------------------------
MoveToTarget(s2),
# ApplyFunction -----------------------
ApplyFunction(
lambda mob: mob.scale(SCALE)
.set_fill(opacity=1)
.set_color(TEAL)
.to_edge(RIGHT),
s3
),
run_time=2
)
self.wait()
There are some animations that come by default and allow us to color, scale or delete an object.
The limitation of this is that you cannot use two class animations at the same Scene.play, unlike .animate or the previous classes that allow us to make several transformations to the same object.
def construct(self):
anims = [FadeToColor,ScaleInPlace,ShrinkToCenter]
mobs = VGroup(*[Text(cls.__name__) for cls in anims])\
.arrange(RIGHT,buff=1)
self.add(mobs)
self.play(
FadeToColor(mobs[0],RED),
ScaleInPlace(mobs[1],2),
ShrinkToCenter(mobs[2])
)
self.wait()
However, there is an animation that is especially complex and that you cannot fully use with transformations, this is: Rotation.
Rotation using methods:
def construct(self):
angles = [10,30,60,90,120]
mobs = VGroup(*[
VGroup(MathTex(f"{angle}^\\circ"),Square())
.arrange(DOWN,buff=1)
for angle in angles
]).arrange(RIGHT,buff=0.7)
self.add(mobs)
self.play(
*[
# mob[0] is the MathTex and
# mob[1] is the Square()
mob[1].animate.rotate(angle*PI/180)
for mob,angle in zip(mobs,angles)
],
run_time=3
)
self.wait()
You can see that the greater the angle, the more deformed the rotation.
This is because, what happens in reality, is that the squares are transformed to another square already rotated, that is, the rotations are not continuous, but it is a simple transformation between two states.
To solve this we can use Rotating:
def construct(self):
angles = [10,30,60,90,120]
mobs = VGroup(*[
VGroup(MathTex(f"{angle}^\\circ"),Square())
.arrange(DOWN,buff=1)
for angle in angles
]).arrange(RIGHT,buff=0.7)
self.add(mobs)
self.play(
*[
# mob[0] is the MathTex and
# mob[1] is the Square()
Rotating(mob[1],radians=angle*PI/180)
for mob,angle in zip(mobs,angles)
],
run_time=3
)
self.wait()
Note
Instead of writing PI/180, DEGREE is usually used, which is exactly the same.
How this animation is done is more complex, we will understand it when we see alpha-type updaters.
In this chapter we will learn some methods and classes that are quite useful to make our animations, here we will conclude all the class animations.
.interpolate_colorAs its name implies, it gives you an interpolation between two colors, you can see the official documentation [https://docs.manim.community/en/stable/reference/manim.utils.color.html?highlight=interpolate_color#manim.utils.color.interpolate_color] to see its definition and the algorithm, You can see in the same documentation other functions related to color in case you want to learn more.
def construct(self):
dots = VGroup(*[Dot() for _ in range(3)])\
.set(width=3).arrange(RIGHT,buff=1)
left_dot, center_dot, right_dot = dots
color1 = "#FF0000"; color2 = "#0000FF"
color3 = interpolate_color(color1,color2,0.5)
# |
# v
# 0 <= alpha <= 1
# color1 color2
print([color1,color2,color3])
# ['#FF0000' '#0000FF' <Color #7f007f>]
left_dot.set_color(color1)
right_dot.set_color(color2)
center_dot.set_color(color3)
self.add(dots)
.set_pointsNote
For this section we are going to use the grid that you created in previous chapters, and it will help us to understand this method.
_as_cornersThe way to make a polyline using the coordinates of the screen we can do the following:
def construct(self):
sg = ScreenGrid() # Here is your Grid
sg.fade(0.5)
def coord(x,y):
return np.array([x,y,0])
points = [
coord(x,y)
for x,y in [
(-6,3),
(-1.5,-1),
(0,0),
(2,-3),
(3.5,2)
]
]
dots = VGroup(*[Dot(p) for p in points])
polyline = VMobject(color=BLUE).set_points_as_corners(points)
self.add(sg,polyline,dots)
_smoothlydef construct(self):
sg = ScreenGrid() # Here is your Grid
sg.fade(0.5)
def coord(x,y):
return np.array([x,y,0])
points = [
coord(x,y)
for x,y in [
(-6,3),
(-1.5,-1),
(0,0),
(2,-3),
(3.5,2)
]
]
dots = VGroup(*[Dot(p) for p in points])
polyline = VMobject(color=BLUE).set_points_smoothly(points)
self.add(sg,polyline,dots)
.point_from_proportionThis is an exclusive method for VMobjects, especially for paths (lines and curves).
Think VMobject (a curve) as a road, as you travel the road you can tell the percentage you have completed. Using this analogy with curves (paths) you can locate a point in a percentage of the path using point_from_proportion.
def construct(self):
path = Line(LEFT,RIGHT) # We will study this below.
path.width = config.frame_width - 2
proportions = np.arange(0,1.1,0.1)
# 0.1, 0.2, 0.3 ... 1.0
prop_text = VGroup(*[
VGroup(
# The first argument to dot is the
# coordinate where it is located,
# so we don't need to use ".move_to"
# in this case.
Dot(path.point_from_proportion(p)),
Text("%.1f"%p,height=0.3).next_to(path.point_from_proportion(p),DOWN),
).set_color(interpolate_color(RED,BLUE,p))
for p in proportions
])
start = Text("Start").next_to(path.get_start(),UP)
end = Text("End").next_to(path.get_end(),UP)
self.add(path,prop_text,start,end)
With regular figures it is easy to see what this method means.
def construct(self):
paths = VGroup(
Line(LEFT,RIGHT), # We will study this below.
Square(),
Circle()
).arrange(RIGHT,buff=0.5)\
.set(width=config.frame_width-2)
def get_proportions(path,proportions=np.arange(0,1.1,0.1)):
return VGroup(*[
Dot(
# The first argument to dot is the
# coordinate where it is located,
# so we don't need to use ".move_to"
# in this case.
path.point_from_proportion(p),
fill_opacity=1-p+0.3,
color=interpolate_color(RED,BLUE,p)
)
for p in proportions
])
def get_start_and_end(path):
return VGroup(
Text("START").next_to(path.get_start(),UP),
Text("END").next_to(path.get_end(),DOWN),
)
vgrp_proportions = VGroup(*[
get_proportions(path)
for path in paths
])
vgrp_start_end = VGroup(*[
get_start_and_end(path)
for path in paths
])
self.add(paths,vgrp_proportions,vgrp_start_end)
This counts for any VMobject.
def construct(self):
sg = ScreenGrid() # Here is your Grid
sg.fade(0.5)
proportions = np.arange(0,1.1,0.1)
# 0.1, 0.2, 0.3 ... 1.0
def coord(x,y):
return np.array([x,y,0])
points = [
coord(x,y)
for x,y in [
(-6,3),
(-1.5,-1),
(0,0),
(2,-3),
(3.5,2)
]
]
polyline = VMobject(color=BLUE).set_points_smoothly(points)
prop_text = VGroup(*[
VGroup(
# The first argument to dot is the
# coordinate where it is located,
# so we don't need to use ".move_to"
# in this case.
Dot(polyline.point_from_proportion(p)),
Text("%.1f"%p,height=0.3)
.next_to(polyline.point_from_proportion(p),DOWN),
).set_color(interpolate_color(RED,BLUE,p))
for p in proportions
])
start = Text("Start").next_to(polyline.get_start(),UP)
end = Text("End").next_to(polyline.get_end(),UP)
.get_subcurveUsing the concept of point_from_proportion, we can get a part of a VMobject by specifying the starting and ending percentage:
def construct(self):
def coord(x,y):
return np.array([x,y,0])
points = [
coord(x,y)
for x,y in [
(-6,3),
(-1.5,-1),
(0,0),
(2,-3),
(3.5,2)
]
]
path = VMobject(color=BLUE).set_points_smoothly(points)
partial_path = path.get_subcurve(0.1,0.9)
partial_path.set_style(stroke_width=10,stroke_color=RED)
self.add(path,partial_path)
.set_color_by_texIn the Tex section we saw that there is an argument that allows us to color certain text, but sometimes we need to color certain parts afterwards, for this we use this method. However, to achieve this effectively we have to isolate the text as we saw it in TransformMatchingTex.
def construct(self):
equation = MathTex(
r"e^x = x^0 + x^1 + \frac{1}{2} x^2 + \frac{1}{6} x^3 + \cdots + \frac{1}{n!} x^n + \cdots",
)
# FAIL
equation.set_color_by_tex("+", YELLOW)
equation.set_color_by_tex("x", BLUE)
equation.width = config.frame_width - 1
self.add(equation)
Succesull coloring:
def construct(self):
equation = MathTex(
r"e^x = x^0 + x^1 + \frac{1}{2} x^2 + \frac{1}{6} x^3 + \cdots + \frac{1}{n!} x^n + \cdots",
substrings_to_isolate=["x","+"]
)
equation.set_color_by_tex("+", YELLOW)
equation.set_color_by_tex("x", BLUE)
equation.width = config.frame_width - 1
self.add(equation)
We could have done this using tex_to_color_map in MathTex instance, but sometimes you will want to color certain parts in different parts of the code, and in this way it can be achieved. But remember that if your formulas are too complex then they will not work.
.save_state and RestoreThis is a very simple method, basically it is about creating a temporary copy in a time state of a Mobject, and after applying certain modifications to it, using Restore we can return to the point where we saved the state.
def construct(self):
FRAME_WIDTH = config.frame_width
text = Tex("Original")\
.set(width=FRAME_WIDTH/2)
text.save_state()
text_2 = Tex("Modified")\
.set(width=FRAME_WIDTH/1.5)\
.set_color(ORANGE)\
.to_corner(DL)
self.add(text)
self.play(Transform(text,text_2))
self.play(
text.animate.shift(RIGHT).rotate(PI/4)
)
self.play(Restore(text))
self.wait(0.7)
.surroundThis method allows us to fit a Mobject in another, this method will be useful when we use indication animations.
def construct(self):
formula = MathTex("x","=","y","+","3").scale(4)
sm1 = Circle().surround(formula[0]) # buffer_factor=1.2) by default
sm2 = Circle().surround(formula[1],buffer_factor=1)
sm3 = Rectangle(color=TEAL).surround(formula[2])
sm4 = Rectangle(color=YELLOW).surround(formula[2],stretch=True) # To fix the ratio
self.add(
formula,
sm1,sm2,sm3,sm4
)
.add_background_rectangleIt is similar to surround, with the difference that this method adds the background to the Mobject.
def construct(self):
number_plane = NumberPlane(axis_config={"include_numbers": True})
matrix = VGroup(*[Text(f"{i}") for i in range(27)])\
.arrange_in_grid(cols=6,buff=1)
_0 = matrix[0]
_16 = matrix[16]
_13 = matrix[13]
_16.add_background_rectangle()
_0.add_background_rectangle(color=RED)
_13.add_background_rectangle(color=YELLOW,buff=0.2)
self.add(
number_plane,matrix
)
If you want to see all the missing methods of the VMobjects you can read the official documentation [https://docs.manim.community/en/stable/reference/manim.mobject.types.vectorized_mobject.VMobject.html?highlight=VMobject#vmobject].
It is equivalent to polyline, only that this VMobject only receives two points, the initial or final. A special parameter of the lines is a buffer, which allows us to add a space between the endpoints.
def construct(self):
sg = ScreenGrid()
l1 = Line(LEFT*3,RIGHT*3).shift(UP)
l2 = Line(LEFT*3,RIGHT*3,buff=1).shift(DOWN)
l3 = Line(LEFT*3,RIGHT*3,buff=2).shift(DOWN*2)
self.add(sg,l1,l2,l3)
Since the lines are a VMobject, then it accepts all the properties of the VMobjects.
The arrows are similar to the lines, although they have a default buff, I’ll leave you some examples here:
def construct(self):
def get_size(s=3):
return [LEFT*s,RIGHT*s]
arrows = VGroup(
Arrow(*get_size()),
Arrow(*get_size(),buff=0),
DoubleArrow(*get_size()),
DoubleArrow(*get_size(),buff=0),
# ---------------
Arrow(*get_size(0.5)),
Arrow(*get_size(0.5),buff=0),
DoubleArrow(*get_size(0.5)),
DoubleArrow(*get_size(0.5),buff=0),
)
arrows.arrange(DOWN,buff=0.5)
self.add(arrows)
You may find that the smaller the arrows, the smaller the tips. They can learn more in the official documentation [https://docs.manim.community/en/stable/reference/manim.mobject.geometry.Arrow.html?highlight=arrows].
In addition to the classic tips, there are also these others:
def construct(self):
# Special tips
from manim.mobject.geometry import (
ArrowTriangleFilledTip,
ArrowTriangleTip
)
tips_set = [
ArrowCircleFilledTip,
ArrowCircleTip,
ArrowSquareFilledTip,
ArrowSquareTip,
ArrowTriangleFilledTip,
ArrowTriangleTip
]
normal_arrow = VGroup(*[
Arrow(LEFT*2,RIGHT*2, tip_shape=ts)
for ts in tips_set
]).arrange(DOWN,buff=0.4)
double_arrow = VGroup(*[
DoubleArrow(LEFT*2,RIGHT*2, tip_shape_start=ts, tip_shape_end=ts)
for ts in tips_set
]).arrange(DOWN,buff=0.4)
normal_arrow_t = Text("Arrow",font="Monospace")
double_arrow_t = Text("DoubleArrow",font="Monospace")
VGroup(
VGroup(normal_arrow_t,normal_arrow).arrange(DOWN),
VGroup(double_arrow_t,double_arrow).arrange(DOWN),
).arrange(RIGHT,buff=1)
self.play(
Write(normal_arrow_t),
Write(double_arrow_t),
*[
GrowArrow(arrow)
for arrow in [*normal_arrow,*double_arrow]
],
run_time=4
)
self.wait(3)
.get_unit_vectorBoth lines and arrows have two very useful methods called .get_vector() and .get_unit_vector(), which, as its name says, returns the vector and unit vector of the line/arrow, we will use it in the next section.
def construct(self):
start = Dot([-2, -1, 0])
end = Dot([ 2, 1, 0])
line = Line(start.get_center(), end.get_center(), color=ORANGE)
down_brace = Brace(line)
left_brace = Brace(line,LEFT,buff=2)
right_brace = Brace(line,RIGHT)
down_brace_tex = down_brace.get_text("Down brace")
left_brace_tex = left_brace.get_text("Left brace")
normal_brace = Brace(line, direction=rotate_vector(line.get_unit_vector(), 90*DEGREES))
# --------------------------------------------------
# normal vector
# rotate_vector is a function that, as the name suggests, rotates a vector.
normal_brace_tex = normal_brace.get_tex("x-x_1")
self.add(
line, start, end,
down_brace, down_brace_tex,
left_brace, left_brace_tex,
normal_brace, normal_brace_tex,
)
This class might seem very redundant, since it is used to write values, generally measurements (meters, seconds, etc), but they will make more sense when we see updaters.
def construct(self):
dgrp = VGroup(
DecimalNumber(0),
DecimalNumber(1,include_sign=True),
DecimalNumber(1,unit="\\rm m"),
DecimalNumber(13.41364,unit="\\rm m",num_decimal_places=3),
DecimalNumber(133414.41364,unit="\\rm m",num_decimal_places=3),
DecimalNumber(133414.41364,unit="\\rm m",num_decimal_places=3,group_with_commas=False),
).scale(2.5).arrange(DOWN)
self.add(dgrp)
Note
There is a subclass of DecimalNumber, which is Integer, and as the name suggests, it has no decimal part.
This is another very useful VMobject, and the base from which the graphs are built. But it has so many configuration options that it is best to read the official documentation [https://docs.manim.community/en/stable/reference/manim.mobject.number_line.NumberLine.html#manim.mobject.number_line.NumberLine].
The most important thing to know is that the numbers are created using DecimalNumber and that there is a method that allows us to obtain a value according to the NumberLine measure, that is NumberLine.n2p(number).
def construct(self):
l0 = NumberLine(
# min max step
x_range=[-10, 10, 1],
length=10,
color=BLUE,
include_numbers=True,
label_direction=UP,
font_size=20,
)
l1 = NumberLine(
x_range=[-10, 10, 2],
unit_size=0.5,
numbers_with_elongated_ticks=[-2, 4],
include_numbers=True,
font_size=24,
)
[num6] = [num for num in l1.numbers if num.number == 6]
num6.set_color(RED)
l1.add(num6)
l2 = NumberLine(
x_range=[-2.5, 2.5 + 0.5, 0.5],
length=12,
# Here they are modifying the parameters of the numbers,
# the number of decimal places.
decimal_number_config={
"num_decimal_places": 1,
"unit": "\\rm m",
"color": TEAL
},
include_numbers=True,
font_size=30,
)
l3 = NumberLine(
x_range=[-5, 5 + 1, 1],
length=6,
include_tip=True,
include_numbers=True,
rotation=10 * DEGREES,
)
line_group = VGroup(l0, l1, l2, l3).arrange(DOWN, buff=1)
pink_dot = Dot(l0.n2p(-3), color=PINK)
orange_dot = Dot(l3.n2p(1.5), color=ORANGE)
self.add(line_group,pink_dot,orange_dot)
Official documentation. [https://docs.manim.community/en/stable/reference/manim.mobject.matrix.html?highlight=Matrix#module-manim.mobject.matrix]
def construct(self):
m0 = Matrix([
["\\pi", 0],
[-1, 1]
])
m1 = Matrix([
["π", "0"],
["-1", "1"]
],
element_to_mobject=Text,
element_to_mobject_config={"font": "Arial"}
)
m2 = IntegerMatrix([
[1.5, 0.],
[12, -1.3]
],
left_bracket="(",
right_bracket=")"
)
m3 = DecimalMatrix(
[[3.456, 2.122], [33.2244, 12.33]],
element_to_mobject_config={"num_decimal_places": 2},
left_bracket="\\{",
right_bracket="\\}")
m4 = MobjectMatrix([
[Circle().scale(0.3), Square().scale(0.3)],
[MathTex("\\pi").scale(2), Star().scale(0.3)]
],
left_bracket="\\langle",
right_bracket="\\rangle"
)
g = Group(m0, m1, m2, m3).arrange_in_grid(buff=1).to_edge(UP)
m4.next_to(g,DOWN,buff=1)
g.add(m4)
self.add(g)
Official documentation. [https://docs.manim.community/en/stable/reference/manim.mobject.table.html?highlight=Tables]
def construct(self):
# Table -------------------------------------------------
t0 = Table([
["First", "Second"],
["Third", "Fourth"]
],
row_labels=[Text("R1"), Text("R2")],
col_labels=[Text("C1"), Text("C2")],
top_left_entry=Text("TOP"))
# (row,col) not start from 0, 1 instead
t0.add_highlighted_cell((2,3), color=GREEN)
# DecimalTable ------------------------------------------
x_vals = np.linspace(-2,2,5)
y_vals = np.exp(x_vals)
t1 = DecimalTable(
[x_vals, y_vals],
row_labels=[MathTex("x"), MathTex("f(x)")],
include_outer_lines=True)
t1.add(t1.get_cell((2,2), color=RED))
# MathTable ---------------------------------------------
t2 = MathTable(
[["+", 0, 5, 10],
[0, 0, 5, 10],
[2, 2, 7, 12],
[4, 4, 9, 14]],
include_outer_lines=True)
t2.get_horizontal_lines()[:3].set_color(ORANGE)
t2.get_vertical_lines()[:3].set_color(ORANGE)
t2.get_vertical_lines()[3].set_color(PINK)
t2.get_vertical_lines()[4].set_color(YELLOW)
t2.get_horizontal_lines()[:3].set_z_index(1)
# MobjectTable ------------------------------------------
cross = VGroup(
Line(UP + LEFT, DOWN + RIGHT),
Line(UP + RIGHT, DOWN + LEFT)
).set_color(BLUE).scale(0.5)
a = Circle().set_color(RED).scale(0.5)
b = cross
t3 = MobjectTable([
[a.copy(),b.copy(),a.copy()],
[b.copy(),a.copy(),a.copy()],
[a.copy(),b.copy(),b.copy()]
])
t3.add(Line(
t3.get_corner(DL), t3.get_corner(UR)
).set_color(RED))
vals = np.arange(1,21).reshape(5,4)
t4 = IntegerTable(
vals,
include_outer_lines=True
)
grp = Group(
Group(t0, t1) .scale(0.5).arrange(buff=1).to_edge(UP, buff=1),
Group(t2, t3, t4).scale(0.5).arrange(buff=1).to_edge(DOWN, buff=1)
)
self.add(grp)
There are mainly thre types of Arcs:
Angle [https://docs.manim.community/en/stable/reference/manim.mobject.geometry.Angle.html]
Arc [https://docs.manim.community/en/stable/reference/manim.mobject.geometry.Arc.html]
ArcBetweenPoints [https://docs.manim.community/en/stable/reference/manim.mobject.geometry.ArcBetweenPoints.html]
Angle has good documentation, I recommend that you see it, there are many examples, for the other two we are going to explain them here.
def construct(self):
number_plane = NumberPlane(axis_config={"include_numbers": True})
arcs = VGroup(
Arc(radius=1),
# ---------------------------
Circle(radius=2,stroke_opacity=0.4),
# start_angle, increment angle
Arc(2, 20*DEGREES, 80*DEGREES),
# ---------------------------
Circle(radius=3,stroke_opacity=0.4,color=BLUE),
Arc(3, 300*DEGREES, (60+90)*DEGREES),
# ---------------------------
Circle(radius=1.5,stroke_opacity=0.4,color=YELLOW,arc_center=[2,2,0]),
Arc(1.5, -30*DEGREES, 60*DEGREES,arc_center=[2,2,0]),
)
dot = Dot(arcs[-1].get_arc_center(),color=YELLOW)
self.add(number_plane,arcs,dot)
def construct(self):
number_plane = NumberPlane(axis_config={"include_numbers": True})
fade_circle = Circle(radius=3)
fade_circle.fade(0.6)
start = Dot(fade_circle.point_at_angle(50*DEGREES),color=ORANGE)
end = Dot(fade_circle.point_at_angle(130*DEGREES),color=PINK)
arcbp = ArcBetweenPoints(
start.get_center(),
end.get_center(),
# 130 - 50 = 80
angle=80*DEGREES
)
self.add(number_plane,fade_circle,arcbp,start,end)
This have many options, I recommend reading the official documentation [https://docs.manim.community/en/stable/reference/manim.mobject.svg.code_mobject.Code.html?highlight=Code].
class MyScene(Scene):
def construct(self):
# It is important to omit the indentation
code = \
'''from manim import *
def custom_mob(mob):
mob.scale(3)
mob.set_color(RED)
class MyScene(Scene):
def construct(self):
s = Square()
self.play(FadeIn(s))
self.play(s.animate.scale(2))
self.play(ApplyFunc(custom_mob,s))
self.wait()
'''
rendered_code = Code(
code=code,
tab_width=4,
background="window",
language="Python",
font="Monospace",
style="monokai"
)
self.add(rendered_code)
def construct(self):
dvgrp = VGroup(
DashedVMobject(Triangle()),
DashedVMobject(Circle(),num_dashes=30),
DashedVMobject(Square(),num_dashes=30,dashed_ratio=0.1),
).arrange(RIGHT).set(width=config.frame_width-1)
self.add(dvgrp)
It creates holes in a VMobject using other VMobjects.
def construct(self):
holes = VGroup(*[
vm.scale(0.5)
for vm in [Triangle(),Circle(),RegularPolygon(6),Star(5)]
]).arrange_in_grid(cols=2,buff=0.5)
vmob_to_cut = Square().scale(2)
c = Cutout(vmob_to_cut, *holes, fill_opacity=1, color=BLUE, stroke_color=RED)
c.scale(1.5)
self.play(DrawBorderThenFill(c), run_time=4)
self.wait()
There are more Mobjects, each new version Mobjects are added and bugs are corrected for those that already exist. I recommend reading the official documentation [https://docs.manim.community/en/stable/reference_index/mobjects.html] to learn more. In this chapter I showed you the most common ones and the ones that you are going to use the most in your animations.
Official documentation [https://docs.manim.community/en/stable/reference/manim.animation.creation.html].
AddTextLetterByLetter [https://docs.manim.community/en/stable/reference/manim.animation.creation.AddTextLetterByLetter.html#manim.animation.creation.AddTextLetterByLetter]: Add letter by letter without animation (like a typewriter), you can control the time between letters.
Write [https://docs.manim.community/en/stable/reference/manim.animation.creation.Write.html?highlight=Write]: Generally used to display text.
Create [https://docs.manim.community/en/stable/reference/manim.animation.creation.Create.html?highlight=Create]: It is used to display VMobjects that preferably have no fill.
DrawBorderThenFill [https://docs.manim.community/en/stable/reference/manim.animation.creation.DrawBorderThenFill.html#manim.animation.creation.DrawBorderThenFill]: Apply a Create without adding the fill, and then add the fill with a FadeIn.
FadeIn [https://docs.manim.community/en/stable/reference/manim.animation.fading.FadeIn.html#manim.animation.fading.FadeIn]: You show a Mobject by adding opacity, you can add it statically, with movement, or with an initial scale.
GrowFromCenter [https://docs.manim.community/en/stable/reference/manim.animation.growing.html]: Show the Mobject from a very small size by making it grow.
GrowFromEdge [https://docs.manim.community/en/stable/reference/manim.animation.growing.GrowFromEdge.html#manim.animation.growing.GrowFromEdge]: Same as GrowFromCenter, but instead of growing the Mobject from the center, it does so from some edge of the Mobject.
GrowFromPoint [https://docs.manim.community/en/stable/reference/manim.animation.growing.GrowFromPoint.html#manim.animation.growing.GrowFromPoint]: Same as the previous ones, only here you are more free to say the exact coordinate of where you want your Mobject to grow.
SpinInFromNothing [https://docs.manim.community/en/stable/reference/manim.animation.growing.SpinInFromNothing.html#manim.animation.growing.SpinInFromNothing]: Same as GrowFromCenter, only it adds a spiral movement.
Note
If you find that few options, don’t worry, we’ll learn to create our own creation animations later.
These animations generate a temporary transformation that is intended to highlight a Mobject.
Official documentation of each one [https://docs.manim.community/en/stable/reference/manim.animation.indication.html].
def construct(self):
indications = [
# Indications that need only the Mobject to be highlighted
ApplyWave,
Circumscribe,
FocusOn,
Indicate,
Wiggle,
# Indications that need another argument
ShowPassingFlash, # This needs a background Mobject, such as Underline or a VMobject with surround
Flash, # This needs a coord
]
names = [Tex(i.__name__).scale(3) for i in indications]
self.add(names[0])
for i in range(len(names)):
if indications[i] is Flash:
# Flash needs a coord
self.play(Flash(UP))
elif indications[i] is ShowPassingFlash:
self.play(ShowPassingFlash(Underline(names[i])))
self.play(
ShowPassingFlash(
RoundedRectangle(color=RED).surround(names[i],stretch=True,buff=1.3),
time_width=0.5
),
run_time=2
)
else:
self.play(indications[i](names[i]))
self.play(AnimationGroup(
FadeOut(names[i], shift=UP*1.5),
FadeIn(names[(i+1)%len(names)], shift=UP*1.5),
))
Note
Again, if you think that there are few animations, we will learn how to create your own animations of this type.
They are basically the same as the creation ones, but with an inverted rate_func=lambda t: func(1-t), like Uncreate or Unwrite.
In the previous versions of Manim there was a special scene called GraphScene, however, in the most recent versions it is no longer necessary, since all the functionalities of GraphScene were transferred to the Axes class, so we can have as many graphs as we want in the same scene (which could not be done in GraphScene).
The Axes are made up of two NumberLines, and you can configure each axis with all the NumberLine options. To define the axes the main thing is to define the scales and the size of the axes.
axes = Axes(
# [start,end,step]
x_range=[-1,5,1],
y_range=[-1,5,1],
# Size of each axis
x_length=6,
y_length=6,
# axis_config: the settings you make here
# will apply to both axis, you have to use the
# NumberLine options
axis_config={"include_numbers": True},
# While axis_config applies to both axis,
# x_axis_config and y_axis_config only apply
# to their respective axis.
x_axis_config={
"color": RED,
"numbers_to_exclude": [2,3],
"decimal_number_config": {
"color": TEAL,
"unit": "\\rm m",
"num_decimal_places": 0
}
},
y_axis_config={
"color": YELLOW,
"include_tip": False,
"decimal_number_config": {
"color": PINK,
"unit": "^\\circ",
"num_decimal_places": 1,
"include_sign": True
}
},
)
Sometimes you will not want to define the length of the axes, sometimes you will want to define the size of each unit, if you do not specify the width of the axes you can define the unit_size.
axes = Axes(
# [start,end,step]
x_range=[-1,5,0.5],
y_range=[-1,5,1],
# Size of each axis
y_length=6,
x_axis_config={
# Instead x_lenght we can define "unit_size"
"unit_size": 2,
"numbers_with_elongated_ticks": list(range(-1,5)),
"longer_tick_multiple": 3,
# gap between axes and numbers
"line_to_number_buff": 0.6,
"numbers_to_include": list(range(-1,5)),
"decimal_number_config": {
"num_decimal_places": 0,
},
"font_size": 70
},
y_axis_config={
"include_numbers": True
},
)
self.add(axes)
There are two concepts to understand, the “coords” reference system and the “points” reference system.
Points: It is the reference system of the camera, and therefore it is absolute, we could say that it is our inertial system, the fixed system.
Coords: It is the reference system of each axis, if you create more than one axes then each one will have its own reference system.
Axes has the method Axes.coords_to_point and Axes.point_to_coords.
coords_to_point/c2p: It receives a two-dimensional vector \((x,y)\), and returns a three-dimensional vector \((x,y,z)\) which refers to the coordinates of your Axes using the reference system of the camera: Points.
point_to_coords/p2c: This method is the inverse, we enter a three-dimensional vector \((x,y,z)\) and it returns a two-dimensional coordinate \((x,y)\) that refers to the axes system coords.
Making graphs with points should be easy for the student, we only need to make a polyline by changing the reference system using c2p, it is left as an exercise.
def construct(self):
axes = Axes(
x_range = (0, 7),
y_range = (0, 5),
x_length = 7,
axis_config={"include_numbers": True},
)
x_values = [0, 1.5, 2, 3, 4, 6.3]
y_values = [1, 3, 2.5, 4, 2, 1.2]
coords = [axes.c2p(x,y) for x,y in zip(x_values,y_values)]
plot = VMobject(color=BLUE).set_points_as_corners(coords)
self.add(axes,plot)
However, Manim already has a functionality that allows us to do the same.
def construct(self):
axes = Axes(
x_range = (0, 7),
y_range = (0, 5),
x_length = 7,
axis_config={"include_numbers": True},
)
axes.center()
line_graph = axes.get_line_graph(
x_values = [0, 1.5, 2, 3, 4, 6.3],
y_values = [1, 3, 2.5, 4, 2, 1.2],
line_color=BLUE,
vertex_dot_style={"stroke_width": 3, "fill_color": RED},
stroke_width = 4,
)
self.add(axes, line_graph)
def construct(self):
x_labels = [
"-\\frac{3\\pi}{2}", # -3pi/2
"-\\pi", # -pi
"-\\frac{\\pi}{2}", # -pi/2
"0", # Blank
"\\frac{\\pi}{2}", # pi/2
"\\pi",# pi
"\\frac{3\\pi}{2}" # 3pi/2
]
axes = Axes(
x_range = (-3*PI/2, 3*PI/2, PI/2),
y_range = (-1.5, 1.5, 0.5),
x_length = 10,
axis_config={"include_tip": False}
)
axes.center()
x_tex_lables = VGroup(*[
MathTex(t).next_to(axes.x_axis.n2p(x),DOWN) if x >= 0 else
# Shift pi<0 labels to left
MathTex(t).next_to(axes.x_axis.n2p(x),DOWN).shift(LEFT*0.2)
for t,x in zip(x_labels,np.arange(-3*PI/2, 3*PI/2+PI/2, PI/2)) if t != "0"
# Ignore 0 value
])
self.add(axes,x_tex_lables)
The plots are only approximations by means of bézier curves, so it is good to indicate the resolution of the graph:
def construct(self):
# Define axes
axes_left = Axes(
x_range = (0, 7, 1),
y_range = (0, 5, 1),
x_length = 7,
axis_config={"include_numbers": True},
)
axes_right = axes_left.copy()
axes = VGroup(axes_left,axes_right).arrange(RIGHT)
axes.width=config.frame_width-1
# Define graphs
function = lambda x: np.sqrt(x)
# good resolution
left_graph = axes_left.get_graph(function, x_range=(0, 7, 0.05))
# bad resolution |-----> Resolution
right_graph = axes_right.get_graph(function,x_range=(0, 7, 3))
self.add(
axes,
left_graph,right_graph
)
The plots are like any VMobject, so it accepts stroke_width, stroke_color, it can be converted to DashedVMobject, etc.
This is another VMobject that has many more options, you already have the knowledge to be able to read the official documentation [https://docs.manim.community/en/stable/reference/manim.mobject.coordinate_systems.CoordinateSystem.html#coordinatesystem].
def construct(self):
# Define axes
axes = Axes(
x_range = (-3, 3, 1),
y_range = (-3, 3, 1),
x_length = 7,
y_length = 7,
axis_config={"include_numbers": True},
)
parametric_func = axes.get_parametric_curve(
lambda t: np.array([
np.cos(t), # x
2*np.sin(t), # y
]),
t_range=(0,2*PI,0.1),
# Domain <---| |----> resolution
color=RED
)
equations = MathTex(r"""
c:\begin{cases}
\cos(t)\\
2\sin(t)\\
t\in [0,2\pi)
\end{cases}
""").scale(1.3)
equations.to_corner(UR)
self.add(axes,parametric_func,equations)
It is the same as Axes but with background lines, it is ideal for making linear transformations, which we will see in the next course.
Official documentation [https://docs.manim.community/en/stable/reference/manim.mobject.coordinate_systems.NumberPlane.html].
With this [https://docs.manim.community/en/stable/reference/manim.mobject.coordinate_systems.CoordinateSystem.html?highlight=riemann#manim.mobject.coordinate_systems.CoordinateSystem.get_riemann_rectangles] we can do it using:
def construct(self):
axes = Axes(
x_range = (-1, 12, 1),
y_range = (-1, 3, 1),
x_length = 12,
y_length = 7,
)
func = axes.get_graph(lambda x: 0.7*np.sqrt(x),x_range=[0,12,0.05])
rects = VGroup(*[
axes.get_riemann_rectangles(
func,
x_range=[1,11],
dx=dx,
input_sample_type="left",
stroke_width=dx if dx > 0.1 else 0.8,
)
.set_color_by_gradient(PURPLE,ORANGE)
.set_stroke(color=BLACK if dx > 0.1 else None)
for dx in [1/(i) for i in range(1,15)]
])
r = rects[0]
self.add(axes,func,r)
for rect in rects[1:]:
self.play(
Transform(r,rect)
)
self.wait(0.3)
self.wait()
Warning
If you are going to create very complex 3D animations, it is not recommended to use Manim, there are other much better tools, such as Blender [https://www.blender.org/]. Use Manim when you really need it. In addition to the fact that a powerful computer is needed to be able to perform these scenes, if not, the rendering of the videos will be excessively slow. In later versions of Manim, the version with OpenGL will be available, which will allow us to use animations with the 3D camera much faster.
In order to graph 3D scenes we need to activate the 3D camera, this is very simple, we just have to build our scenes using ThreeDScene instead of Scene.
class My3DScene(ThreeDScene):
pass
If you do this and make any animation, you will not notice the difference, for this you have to move the camera. For this, we are going to use the 3D axes, and we are going to compare by changing the camera angles.
From now on we are going to skip the first 4 lines:
class My3DScene(ThreeDScene):
def construct(self):
axes_3d = ThreeDAxes()
self.add(axes_3d)
Recapping, the angles are like this:
Note that by default theta=-90*DEGREES.
You can add animations when you perform the camera movement:
If you are going to graph surfaces, the axes must have a unit_size=1, that is, each unit of the camera must be a unit of the graphs, this is because ThreeDAxes does not have a method to create surfaces, so the surfaces use the camera’s units of measure.
If you are only going to graph parametric curves or vectors you can use any unit_size, you only have to be careful in the Z axis, since Axes.c2p does not contemplate the Z coordinate:
def construct(self):
axes_3d = ThreeDAxes(
# unit_size=1 in Z axis
z_range=(-3,3,1),
z_length=6,
)
self.set_camera_orientation(phi=70*DEGREES,theta=240*DEGREES)
main_line = Line(ORIGIN,axes_3d.c2p(4,3)+2*OUT,color=RED)
vertical_line = DashedLine(axes_3d.c2p(4,0),axes_3d.c2p(4,3))
horizontal_line = DashedLine(axes_3d.c2p(0,3),axes_3d.c2p(4,3))
fall_line = DashedLine(axes_3d.c2p(4,3),axes_3d.c2p(4,3)+OUT*2)
def construct(self):
axes_3d = ThreeDAxes()
func = axes_3d.get_parametric_curve(
lambda t: np.array([
2*np.cos(t),
3*np.sin(t),
t/3
]),
t_range=(-2*PI,2*PI,0.01),
color=RED
)
self.set_camera_orientation(phi=70*DEGREES,theta=240*DEGREES)
self.add(
axes_3d,
func
)
self.wait(0.5)
self.move_camera(theta=120*DEGREES,run_time=6,rate_func=linear)
self.wait(0.5)
self.move_camera(theta=90*DEGREES,phi=0,run_time=2,rate_func=smooth)
self.wait(0.5)
If you are going to graph Surfaces then it is necessary to normalize all unit_size on the axes to 1.
def construct(self):
axes_3d = ThreeDAxes(
x_range=(-6,6,1),
x_length=12,
y_range=(-5,5,1),
y_length=10,
z_range=(-3,3,1),
z_length=6,
)
self.set_camera_orientation(phi=70*DEGREES,theta=240*DEGREES)
surface = ParametricSurface(
lambda u, v: np.array([
np.cos(TAU * v),
np.sin(TAU * v),
2 * (1 - u)
]),
).fade(0.5)
paraboloid = ParametricSurface(
lambda u, v: np.array([
np.cos(v)*u,
np.sin(v)*u,
u**2
]),
v_range=(0,TAU),
).fade(0.5)
para_hyp = ParametricSurface(
lambda u, v: np.array([
u,
v,
u**2-v**2
]),
u_range=(-2,2),
v_range=(-2,2),
).fade(0.5)
cone = ParametricSurface(
lambda u, v: np.array([
u*np.cos(v),
u*np.sin(v),
u
]),
u_range=(-2,2),
v_range=(0,TAU),
)
sphere = ParametricSurface(
lambda u, v: np.array([
1.5*np.cos(u)*np.cos(v),
1.5*np.cos(u)*np.sin(v),
1.5*np.sin(u)
]),
#Resolution of the surfaces
u_range=(-PI/2,PI/2),
v_range=(0,TAU),
)
self.add(
axes_3d,surface
)
self.play(
Transform(surface, paraboloid)
)
self.wait(0.5)
self.play(
Transform(surface, para_hyp)
)
self.wait(0.5)
self.play(
Transform(surface, cone)
)
self.wait(0.5)
self.play(
Transform(surface, sphere)
)
self.wait(0.5)
Using ThreeDScene.add_fixed_in_frame_mobjects you make mobs always look at the screen fixed.
def construct(self):
axes_3d = ThreeDAxes(
# unit_size=1 in Z axis
z_range=(-3,3,1),
z_length=6,
)
self.set_camera_orientation(phi=70*DEGREES,theta=240*DEGREES)
# Arrows3Ds are surfaces, so the more you add, the longer it will take to render.
main_line = Arrow3D(ORIGIN,axes_3d.c2p(4,3)+2*OUT,color=GREEN,height=0.7,base_radius=0.2)
vertical_line = Arrow3D(axes_3d.c2p(4,0),axes_3d.c2p(4,3))
horizontal_line = Arrow3D(axes_3d.c2p(0,3),axes_3d.c2p(4,3))
fall_line = Arrow3D(axes_3d.c2p(4,3),axes_3d.c2p(4,3)+OUT*2)
self.add(
axes_3d,
main_line,
vertical_line,
horizontal_line,
fall_line,
)
self.wait(0.5)
self.move_camera(theta=30*DEGREES,run_time=4)
self.wait()
Updaters are functions that are applied to a Mobject and that are updated every frame.
Question: How would we make sure that the square is always above the circle without having to manipulate the square?
Basically, we need the square.next_to(circle,UP) to apply each frame of the animation, for this we can use the Updaters.
If the functions are simple then we can use anonymous functions to make it shorter.
And this updater is going to run until we pause and delete it.
Pause updaters: Mobject.suspend_updating()
Restore updaters: Mobject.resume_updating()
Delete all updaters: Mobject.clear_updaters()
Yes, you can add more updaters, each Mobject has an attribute (a list) where it stores its updaters, so you can add or remove more updaters.
.becomeNow imagine that we want the Brace to change in size depending on the line.
There are some Mobjects that you can change their shape through their methods, but the most general way is to use .become.
The easiest way to understand .become is by making the analogy with Transform, basically .become is an instant Transform, and I can demonstrate it with the following code.
def construct(self):
c = Circle().scale(2)
s = Square().scale(2)
c.generate_target()
c.target.become(s) # <-- it's an instant transformation
self.add(c)
self.play(MoveToTarget(c))
self.wait()
Using this concept, we can instantly transform a Mobject each frame, so it will give the feeling that the transformation is smooth.
def construct(self):
line = Line(LEFT,RIGHT)
def get_brace(mob):
mob.become(
Brace(line,UP)
)
brace = VMobject() # This is the initial status,
# when we add the updater it will update
brace.add_updater(get_brace)
self.add(line,brace)
self.wait(0.5)
self.play(line.animate.scale(3))
self.wait(0.5)
self.play(line.animate.scale(0.8))
self.wait(0.5)
self.play(line.animate.scale(1.3))
self.wait(0.5)
Some Mobjects already have methods that help to deform them, for example, lines and vectors have the put_start_and_end_on method:
def construct(self):
start_dot = Dot(LEFT,color=RED)
end_dot = Dot(RIGHT,color=TEAL)
def update_line(mob):
mob.put_start_and_end_on(start_dot.get_center(),end_dot.get_center())
line = Line() # Also it works with Arrow
line.add_updater(update_line)
self.add(line, start_dot, end_dot)
self.wait(0.5)
self.play(start_dot.animate.shift(LEFT*2+UP))
self.wait(0.5)
self.play(end_dot.animate.shift(RIGHT+DOWN*0.5))
self.wait(0.5)
self.play(
start_dot.animate.shift(RIGHT*0.5+DOWN*2),
end_dot.animate.shift(RIGHT+UP*3).scale(5)
)
self.wait(0.5)
self.play(
start_dot.animate.shift(LEFT*3).scale(5),
end_dot.animate.shift(DOWN*2)
)
self.wait(0.5)
This class does not appear on the screen, it is simply a tool that allows us to increase or decrease numerical values.
def construct(self):
nl = NumberLine(include_numbers=True)
selector = Triangle(fill_opacity=1).scale(0.2).rotate(PI/3)
vt = ValueTracker(0) # <-- needs a start value
def update_selector(mob):
mob.next_to(nl.n2p(vt.get_value()),UP,buff=0)
# --------------
# In this way we can acces to the ValueTracker value
selector.add_updater(update_selector)
self.add(nl,selector)
self.wait(0.5)
self.play(
vt.animate.set_value(4),
run_time=2
)
self.wait(0.5)
self.play(
vt.animate.set_value(-1),
run_time=2
)
self.wait(0.5)
self.play(
vt.animate.set_value(-7),
run_time=2
)
self.wait(0.5)
self.play(
vt.animate.set_value(7),
run_time=3
)
self.wait(0.5)
As an exercise try to duplicate this animation:
Now we will understand the value of DecimalNumber, which is similar to ValueTracker, only that it can be displayed on the screen:
def construct(self):
nl = NumberLine(include_numbers=True)
selector = Triangle(fill_opacity=1).scale(0.2).rotate(PI/3)
dn = DecimalNumber(0) # <-- needs a start value
def update_selector(mob):
mob.next_to(nl.n2p(dn.get_value()),UP,buff=0)
# --------------
# In this way we can acces to the DecimalNumber value
def update_dn(mob):
mob.next_to(selector,UP,buff=0.1)
selector.add_updater(update_selector)
dn.add_updater(update_dn)
self.add(nl,selector,dn)
self.wait(0.5)
self.play(
ChangeDecimalToValue(dn, 4),
run_time=2
)
self.wait(0.5)
self.play(
ChangeDecimalToValue(dn, -1),
run_time=2
)
self.wait(0.5)
self.play(
ChangeDecimalToValue(dn, -4),
run_time=2
)
self.wait(0.5)
self.play(
ChangeDecimalToValue(dn, 7),
run_time=3
)
self.wait(0.5)
Sometimes it is easier for us to create an updater that modifies several Mobjects, this can be done using Groups.
def construct(self):
nl = NumberLine(include_numbers=True)
selector = Triangle(fill_opacity=1,color=RED).scale(0.2).rotate(PI/3)
dn = DecimalNumber(0)
update_grp = VGroup(selector, dn)
def update_vgrp(vgrp):
s,d = vgrp
s.next_to(nl.n2p(dn.get_value()),UP,buff=0)
d.next_to(selector,UP,buff=0.1)
update_grp.add_updater(update_vgrp)
self.add(nl,update_grp) # <-- add grp complete, not each element
# Not use self.add(nl, selecttor, dn)
self.wait(0.5)
self.play(
ChangeDecimalToValue(dn, 4),
run_time=2
)
self.wait(0.5)
self.play(
ChangeDecimalToValue(dn, -1),
run_time=2
)
self.wait(0.5)
self.play(
ChangeDecimalToValue(dn, -4),
run_time=2
)
self.wait(0.5)
self.play(
ChangeDecimalToValue(dn, 7),
run_time=3
)
self.wait(0.5)